File Coverage

blib/lib/Alzabo/Runtime/Schema.pm
Criterion Covered Total %
statement 30 239 12.5
branch 0 106 0.0
condition 0 21 0.0
subroutine 10 37 27.0
pod 22 23 95.6
total 62 426 14.5


line stmt bran cond sub pod time code
1             package Alzabo::Runtime::Schema;
2              
3 11     11   67 use strict;
  11         26  
  11         446  
4 11     11   63 use vars qw($VERSION);
  11         35  
  11         501  
5              
6 11     11   67 use Alzabo::Exceptions ( abbr => [ qw( logic_exception params_exception ) ] );
  11         20  
  11         186  
7 11     11   67 use Alzabo::Runtime;
  11         24  
  11         277  
8 11     11   65 use Alzabo::Utils;
  11         20  
  11         434  
9              
10 11     11   62 use Params::Validate qw( :all );
  11         23  
  11         4382  
11             Params::Validate::validation_options( on_fail => sub { params_exception join '', @_ } );
12              
13 11     11   65 use base qw(Alzabo::Schema);
  11         30  
  11         8725  
14              
15             $VERSION = 2.0;
16              
17             1;
18              
19             sub load_from_file
20             {
21 0     0 1   my $class = shift;
22              
23 0           my $self = $class->_load_from_file(@_);
24              
25 0           $self->prefetch_all_but_blobs;
26              
27 0           return $self;
28             }
29              
30             sub _schema_file_type
31             {
32 0     0     return 'runtime';
33             }
34              
35             sub user
36             {
37 0     0 1   my $self = shift;
38              
39 0           return $self->{user};
40             }
41              
42             sub password
43             {
44 0     0 1   my $self = shift;
45              
46 0           return $self->{password};
47             }
48              
49             sub host
50             {
51 0     0 1   my $self = shift;
52              
53 0           return $self->{host};
54             }
55              
56             sub port
57             {
58 0     0 1   my $self = shift;
59              
60 0           return $self->{port};
61             }
62              
63             sub referential_integrity
64             {
65 0     0 1   my $self = shift;
66              
67 0 0         return defined $self->{maintain_integrity} ? $self->{maintain_integrity} : 0;
68             }
69              
70             sub set_db_schema_name
71             {
72 0     0 0   my $self = shift;
73              
74 0           $self->{db_schema_name} = shift;
75             }
76              
77             sub set_user
78             {
79 0     0 1   my $self = shift;
80              
81 0           $self->{user} = shift;
82             }
83              
84             sub set_password
85             {
86 0     0 1   my $self = shift;
87              
88 0           $self->{password} = shift;
89             }
90              
91             sub set_host
92             {
93 0     0 1   my $self = shift;
94              
95 0           $self->{host} = shift;
96             }
97              
98             sub set_port
99             {
100 0     0 1   my $self = shift;
101              
102 0           $self->{port} = shift;
103             }
104              
105             sub set_referential_integrity
106             {
107 0     0 1   my $self = shift;
108 0           my $val = shift;
109              
110 0 0         $self->{maintain_integrity} = $val if defined $val;
111             }
112              
113             sub set_quote_identifiers
114             {
115 0     0 1   my $self = shift;
116 0           my $val = shift;
117              
118 0 0         $self->{quote_identifiers} = $val if defined $val;
119             }
120              
121             sub connect
122             {
123 0     0 1   my $self = shift;
124              
125 0           my %p;
126 0 0         $p{user} = $self->user if defined $self->user;
127 0 0         $p{password} = $self->password if defined $self->password;
128 0 0         $p{host} = $self->host if defined $self->host;
129 0 0         $p{port} = $self->port if defined $self->port;
130 0           $self->driver->connect( %p, @_ );
131              
132             # $self->set_referential_integrity( ! $self->driver->supports_referential_integrity );
133             }
134              
135             sub disconnect
136             {
137 0     0 1   my $self = shift;
138              
139 0           $self->driver->disconnect;
140             }
141              
142             sub one_row
143             {
144             # could be replaced with something potentially more efficient
145 0     0 1   return shift->join(@_)->next;
146             }
147              
148 11         16008 use constant JOIN_SPEC => { join => { type => ARRAYREF | OBJECT,
149             optional => 1 },
150             tables => { type => ARRAYREF | OBJECT,
151             optional => 1 },
152             select => { type => ARRAYREF | OBJECT,
153             optional => 1 },
154             where => { type => ARRAYREF,
155             optional => 1 },
156             order_by => { type => ARRAYREF | HASHREF | OBJECT,
157             optional => 1 },
158             limit => { type => SCALAR | ARRAYREF,
159             optional => 1 },
160             distinct => { type => ARRAYREF | OBJECT,
161             optional => 1 },
162             quote_identifiers => { type => BOOLEAN,
163             optional => 1 },
164 11     11   73 };
  11         25  
165              
166             sub join
167             {
168 0     0 1   my $self = shift;
169 0           my %p = validate( @_, JOIN_SPEC );
170              
171 0   0       $p{join} ||= delete $p{tables};
172 0 0         $p{join} = [ $p{join} ] unless Alzabo::Utils::is_arrayref( $p{join} );
173              
174 0           my @tables;
175              
176 0 0         if ( Alzabo::Utils::is_arrayref( $p{join}->[0] ) )
177             {
178             # flattens the nested structure and produces a unique set of
179             # tables
180 0           @tables = values %{ { map { $_ => $_ }
  0            
  0            
181 0           grep { Alzabo::Utils::safe_isa( $_, 'Alzabo::Table' ) }
182 0           map { @$_ } @{ $p{join} } } };
  0            
183             }
184             else
185             {
186 0           @tables = grep { Alzabo::Utils::safe_isa($_, 'Alzabo::Table') } @{ $p{join} };
  0            
  0            
187             }
188              
189 0 0         if ( $p{distinct} )
190             {
191 0 0         $p{distinct} =
192             Alzabo::Utils::is_arrayref( $p{distinct} ) ? $p{distinct} : [ $p{distinct} ];
193             }
194              
195 0 0         if ( $p{order_by} )
196             {
197 0 0         $p{order_by} =
    0          
198             Alzabo::Utils::is_arrayref( $p{order_by} )
199             ? $p{order_by}
200             : $p{order_by}
201             ? [ $p{order_by} ]
202             : undef;
203             }
204              
205             # We go in this order: $p{select}, $p{distinct}, @tables
206 0           my @select_tables = ( $p{select} ?
207             ( Alzabo::Utils::is_arrayref( $p{select} ) ?
208 0           @{ $p{select} } : $p{select} ) :
209             $p{distinct} ?
210 0 0         @{ $p{distinct} } :
    0          
    0          
211             @tables );
212              
213 0           my $sql = Alzabo::Runtime::sqlmaker( $self, \%p );
214              
215 0           my @select_cols;
216 0 0         if ( $p{distinct} )
217             {
218 0           my %distinct = map { $_ => 1 } @{ $p{distinct} };
  0            
  0            
219              
220             # hack so distinct is not treated as a function, just a
221             # bareword in the SQL
222 0 0         @select_cols = ( 'DISTINCT',
223 0           map { ( $_->primary_key,
224             $_->prefetch ?
225             $_->columns( $_->prefetch ) :
226             () ) }
227 0           @{ $p{distinct} }
228             );
229              
230 0           foreach my $t (@select_tables)
231             {
232 0 0         next if $distinct{$t};
233 0           push @select_cols, $t->primary_key;
234              
235 0 0         push @select_cols, $t->columns( $t->prefetch ) if $t->prefetch;
236             }
237              
238 0 0 0       if ( $p{order_by} && $sql->distinct_requires_order_by_in_select )
239             {
240 0           my %select_cols = map { $_ => 1 } @select_cols;
  0            
241 0           push @select_cols, grep { ref } @{ $p{order_by} };
  0            
  0            
242             }
243              
244 0           @select_tables = ( @{ $p{distinct} }, grep { ! $distinct{$_} } @select_tables );
  0            
  0            
245             }
246             else
247             {
248 0 0         @select_cols =
249 0           ( map { ( $_->primary_key,
250             $_->prefetch ?
251             $_->columns( $_->prefetch ) :
252             () ) }
253             @select_tables );
254             }
255              
256 0           $sql->select(@select_cols);
257              
258 0           $self->_join_all_tables( sql => $sql,
259             join => $p{join} );
260              
261 0 0         Alzabo::Runtime::process_where_clause( $sql, $p{where} ) if exists $p{where};
262              
263 0 0         Alzabo::Runtime::process_order_by_clause( $sql, $p{order_by} )
264             if $p{order_by};
265              
266 0 0         $sql->limit( ref $p{limit} ? @{ $p{limit} } : $p{limit} ) if $p{limit};
  0 0          
267              
268 0           $sql->debug(\*STDERR) if Alzabo::Debug::SQL;
269 0           print STDERR Devel::StackTrace->new if Alzabo::Debug::TRACE;
270              
271 0           my $statement = $self->driver->statement( sql => $sql->sql,
272             bind => $sql->bind );
273              
274 0 0         if (@select_tables == 1)
275             {
276 0           return Alzabo::Runtime::RowCursor->new
277             ( statement => $statement,
278             table => $select_tables[0]->real_table,
279             );
280             }
281             else
282             {
283 0           return Alzabo::Runtime::JoinCursor->new
284             ( statement => $statement,
285 0           tables => [ map { $_->real_table } @select_tables ],
286             );
287             }
288             }
289              
290             sub row_count
291             {
292 0     0 1   my $self = shift;
293 0           my %p = @_;
294              
295 0           return $self->function( select => Alzabo::Runtime::sqlmaker( $self, \%p )->COUNT('*'),
296             %p,
297             );
298             }
299              
300             sub function
301             {
302 0     0 1   my $self = shift;
303 0           my %p = @_;
304              
305 0           my $sql = $self->_select_sql(%p);
306              
307             my $method =
308 0 0 0       Alzabo::Utils::is_arrayref( $p{select} ) && @{ $p{select} } > 1 ? 'rows' : 'column';
309              
310 0           $sql->debug(\*STDERR) if Alzabo::Debug::SQL;
311 0           print STDERR Devel::StackTrace->new if Alzabo::Debug::TRACE;
312              
313 0           return $self->driver->$method( sql => $sql->sql,
314             bind => $sql->bind );
315             }
316              
317             sub select
318             {
319 0     0 1   my $self = shift;
320              
321 0           my $sql = $self->_select_sql(@_);
322              
323 0           $sql->debug(\*STDERR) if Alzabo::Debug::SQL;
324 0           print STDERR Devel::StackTrace->new if Alzabo::Debug::TRACE;
325              
326 0           return $self->driver->statement( sql => $sql->sql,
327             bind => $sql->bind );
328             }
329              
330 11         6611 use constant _SELECT_SQL_SPEC => { join => { type => ARRAYREF | OBJECT,
331             optional => 1 },
332             tables => { type => ARRAYREF | OBJECT,
333             optional => 1 },
334             select => { type => SCALAR | ARRAYREF | OBJECT,
335             optional => 1 },
336             where => { type => ARRAYREF,
337             optional => 1 },
338             group_by => { type => ARRAYREF | HASHREF | OBJECT,
339             optional => 1 },
340             order_by => { type => ARRAYREF | HASHREF | OBJECT,
341             optional => 1 },
342             having => { type => ARRAYREF,
343             optional => 1 },
344             limit => { type => SCALAR | ARRAYREF,
345             optional => 1 },
346             quote_identifiers => { type => BOOLEAN,
347             optional => 1 },
348 11     11   79 };
  11         22  
349              
350             sub _select_sql
351             {
352 0     0     my $self = shift;
353 0           my %p = validate( @_, _SELECT_SQL_SPEC );
354              
355 0   0       $p{join} ||= delete $p{tables};
356 0 0         $p{join} = [ $p{join} ] unless Alzabo::Utils::is_arrayref( $p{join} );
357              
358 0           my @tables;
359              
360 0 0         if ( Alzabo::Utils::is_arrayref( $p{join}->[0] ) )
361             {
362             # flattens the nested structure and produces a unique set of
363             # tables
364 0           @tables = values %{ { map { $_ => $_ }
  0            
  0            
365 0           grep { Alzabo::Utils::safe_isa( 'Alzabo::Table', $_ ) }
366 0           map { @$_ } @{ $p{join} } } };
  0            
367             }
368             else
369             {
370 0           @tables = grep { Alzabo::Utils::safe_isa( 'Alzabo::Table', $_ ) } @{ $p{join} };
  0            
  0            
371             }
372              
373 0 0         my @funcs = Alzabo::Utils::is_arrayref( $p{select} ) ? @{ $p{select} } : $p{select};
  0            
374              
375 0           my $sql = ( Alzabo::Runtime::sqlmaker( $self, \%p )->
376             select(@funcs) );
377              
378 0           $self->_join_all_tables( sql => $sql,
379             join => $p{join} );
380              
381 0 0         Alzabo::Runtime::process_where_clause( $sql, $p{where} )
382             if exists $p{where};
383              
384 0 0         Alzabo::Runtime::process_group_by_clause( $sql, $p{group_by} )
385             if exists $p{group_by};
386              
387 0 0         Alzabo::Runtime::process_having_clause( $sql, $p{having} )
388             if exists $p{having};
389              
390 0 0         Alzabo::Runtime::process_order_by_clause( $sql, $p{order_by} )
391             if exists $p{order_by};
392              
393 0 0         $sql->limit( ref $p{limit} ? @{ $p{limit} } : $p{limit} ) if $p{limit};
  0 0          
394              
395 0           return $sql;
396             }
397              
398 11         15007 use constant _JOIN_ALL_TABLES_SPEC => { join => { type => ARRAYREF },
399 11     11   71 sql => { isa => 'Alzabo::SQLMaker' } };
  11         33  
400              
401             sub _join_all_tables
402             {
403 0     0     my $self = shift;
404 0           my %p = validate( @_, _JOIN_ALL_TABLES_SPEC );
405              
406 0           my @from;
407             my @joins;
408              
409             # outer join given as only join
410 0 0         $p{join} = [ $p{join} ] unless ref $p{join}->[0];
411              
412             # A structure like:
413             #
414             # [ [ $t_1 => $t_2 ],
415             # [ $t_1 => $t_3, $fk ],
416             # [ left_outer_join => $t_3 => $t_4 ],
417             # [ left_outer_join => $t_3 => $t_5, undef, [ $where_clause ] ]
418             #
419 0 0         if ( Alzabo::Utils::is_arrayref( $p{join}->[0] ) )
420             {
421 0           my %map;
422             my %tables;
423              
424 0           foreach my $set ( @{ $p{join} } )
  0            
425             {
426             # we take some care not to change the contents of $set,
427             # because the caller may reuse the variable being
428             # referenced, and changes here could break that.
429              
430             # XXX - improve
431 0 0         params_exception
432             'The table map must contain only two tables per array reference'
433             if @$set > 5;
434              
435 0           my @tables;
436 0 0         if ( ! ref $set->[0] )
437             {
438 0 0         $set->[0] =~ /^(right|left|full)_outer_join$/i
439             or params_exception "Invalid join type: $set->[0]";
440              
441 0           @tables = @$set[1,2];
442              
443 0           push @from, [ $1, @tables, @$set[3, 4] ];
444             }
445             else
446             {
447 0           @tables = @$set[0,1];
448              
449 0           push @from, grep { ! exists $tables{ $_->alias_name } } @tables;
  0            
450 0           push @joins, [ @tables, $set->[2] ];
451             }
452              
453             # Track the tables we've seen
454 0           @tables{ $tables[0]->alias_name, $tables[1]->alias_name } = (1, 1);
455              
456             # Track their relationships
457 0           push @{ $map{ $tables[0]->alias_name } }, $tables[1]->alias_name;
  0            
458 0           push @{ $map{ $tables[1]->alias_name } }, $tables[0]->alias_name;
  0            
459             }
460              
461             # just get one key to start with
462 0           my ($key) = (each %tables)[0];
463 0           delete $tables{$key};
464 0           my @t = @{ delete $map{$key} };
  0            
465 0           while (my $t = shift @t)
466             {
467 0           delete $tables{$t};
468 0 0         push @t, @{ delete $map{$t} } if $map{$t};
  0            
469             }
470              
471             logic_exception
472 0 0         "The specified table parameter does not connect all the tables involved in the join"
473             if keys %tables;
474             }
475             # A structure like:
476             #
477             # [ $t_1 => $t_2 => $t_3 => $t_4 ]
478             #
479             else
480             {
481 0           for (my $x = 0; $x < @{ $p{join} } - 1; $x++)
  0            
482             {
483 0           push @joins, [ $p{join}->[$x], $p{join}->[$x + 1] ];
484             }
485              
486 0           @from = @{ $p{join} };
  0            
487             }
488              
489 0           $p{sql}->from(@from);
490              
491 0 0         return unless @joins;
492              
493 0           foreach my $join (@joins)
494             {
495 0           $self->_join_two_tables( $p{sql}, @$join );
496             }
497              
498 0           $p{sql}->subgroup_end;
499             }
500              
501             sub _join_two_tables
502             {
503 0     0     my $self = shift;
504 0           my ($sql, $table_1, $table_2, $fk) = @_;
505              
506 0 0 0       my $op = $sql->last_op eq 'and' || $sql->last_op eq 'condition' ? 'and' : 'where';
507              
508 0 0         if ($fk)
509             {
510 0 0 0       unless ( $fk->table_from eq $table_1 && $fk->table_to eq $table_2 )
511             {
512 0 0 0       if ( $fk->table_from eq $table_2 && $fk->table_to eq $table_1 )
513             {
514 0           $fk = $fk->reverse;
515             }
516             else
517             {
518 0           params_exception
519             ( "The foreign key given to join together " .
520             $table_1->alias_name .
521             " and " . $table_2->alias_name .
522             " does not represent a relationship between those two tables" );
523             }
524             }
525             }
526             else
527             {
528 0           my @fk = $table_1->foreign_keys_by_table($table_2);
529              
530 0 0         logic_exception
531             ( "The " . $table_1->name .
532             " table has no foreign keys to the " .
533             $table_2->name . " table" )
534             unless @fk;
535              
536 0 0         logic_exception
537             ( "The " . $table_1->name .
538             " table has more than 1 foreign key to the " .
539             $table_2->name . " table" )
540             if @fk > 1;
541              
542 0           $fk = $fk[0];
543             }
544              
545 0           foreach my $cp ( $fk->column_pair_names )
546             {
547 0 0         if ( $op eq 'where' )
548             {
549             # first time through loop only
550 0           $sql->where;
551 0           $sql->subgroup_start;
552 0           $sql->condition( $table_1->column( $cp->[0] ), '=', $table_2->column( $cp->[1] ) );
553             }
554             else
555             {
556 0           $sql->$op( $table_1->column( $cp->[0] ), '=', $table_2->column( $cp->[1] ) );
557             }
558 0           $op = 'and';
559             }
560             }
561              
562             sub prefetch_all
563             {
564 0     0 1   my $self = shift;
565              
566 0           $_->set_prefetch( $_->columns ) for $self->tables;
567             }
568              
569             sub prefetch_all_but_blobs
570             {
571 0     0 1   my $self = shift;
572              
573 0           $_->set_prefetch( grep { ! $_->is_blob } $_->columns ) for $self->tables;
  0            
574             }
575              
576             sub prefetch_none
577             {
578 0     0 1   my $self = shift;
579              
580 0           $_->set_prefetch() for $self->tables;
581             }
582              
583             __END__