File Coverage

blib/lib/DBIx/ORM/Declarative/Table.pm
Criterion Covered Total %
statement 18 254 7.0
branch 0 126 0.0
condition 0 43 0.0
subroutine 6 23 26.0
pod 6 10 60.0
total 30 456 6.5


line stmt bran cond sub pod time code
1             package DBIx::ORM::Declarative::Table;
2              
3 1     1   6 use strict;
  1         2  
  1         38  
4 1     1   6 use Carp;
  1         2  
  1         84  
5 1     1   6 use Scalar::Util qw(reftype);
  1         2  
  1         147  
6              
7 1     1   14 use vars qw(@ISA);
  1         2  
  1         811  
8             @ISA = qw(DBIx::ORM::Declarative::Schema);
9              
10             # The default where clause prefix:
11 0     0     sub _where_prefix { ''; }
12              
13             # We're not a joing
14 0     0     sub _isjoin { 0; }
15              
16             # Handle a rollback operation
17             sub __do_rollback
18             {
19 0     0     my ($self, @ops) = @_;
20 0           my $handle = $self->handle;
21 0 0 0       carp "Can't roll back: no database" and return unless $handle;
22              
23             # Do the passed-in undo operations
24 0           $handle->do($_) foreach @ops;
25              
26             # Turn off warnings
27 0           local($SIG{__WARN__}) = $self->w__noop;
28 0           $handle->rollback;
29             }
30              
31             # The constraints
32             # Check for a number
33             sub isnumber
34             {
35 0     0 0   my ($self, $value) = @_;
36 0 0         return unless defined $value;
37 0           $value =~ /^(?:\.\d+)|(?:\d+(?:\.\d+)?)$/;
38             }
39              
40             # Check for any defined string
41             sub isstring
42             {
43 0     0 0   my ($self, $value) = @_;
44 0 0         defined $value and length $value;
45             }
46              
47             # Check for a number, or nothing
48             sub isnullablenumber
49             {
50 0     0 0   my ($self, $value) = @_;
51 0 0         return defined $value?$self->isnumber($value):1;
52             }
53              
54             # Always passes
55 0     0 0   sub isnullablestring { 1; }
56              
57             # How to handle operators in a search operation
58 1         149 use constant criteriamap =>
59             {
60             eq => [ '=', 1 ],
61             ne => [ '!=', 1 ],
62             gt => [ '>', 1 ],
63             lt => [ '<', 1 ],
64             ge => [ '>=', 1 ],
65             le => [ '<=', 1 ],
66             isnull => [ 'IS NULL', 0 ],
67             notnull => [ 'IS NOT NULL', 0 ],
68             in => [ undef, 'IN' ],
69             notin => [ undef, 'NOT IN' ],
70             like => [ 'LIKE', 1 ],
71             notlike => [ 'NOT LIKE', 1 ],
72 1     1   7 } ;
  1         2  
73              
74             # How many parameters are taken in limit and order clauses
75 1         4807 use constant critexceptions =>
76             {
77             'limit by' => 2,
78             'order by' => 1,
79 1     1   7 };
  1         2  
80              
81             # Look for a 'limit by' clause
82             sub __find_limit
83             {
84 0     0     my ($self, @critera) = @_;
85 0           my ($offset, $count) = $self->__do_special_purpose('limit by', @critera);
86 0 0         return unless defined $count;
87 0           my $lc = $self->_limit_clause;
88 0           $lc =~ s/%offset%/$offset/g;
89 0           $lc =~ s/%count%/$count/g;
90 0           $lc;
91             }
92              
93             # Handle an "order by" clause we've found
94             sub __find_orderby
95             {
96 0     0     my ($self, @critera) = @_;
97 0           my ($colref) = $self->__do_special_purpose('order by', @critera);
98 0 0         return unless ref $colref;
99 0           'ORDER BY ' . join(',', @$colref);
100             }
101              
102             # Pull out a special-purpose clause
103             sub __do_special_purpose
104             {
105 0     0     my ($self, $clause, @criteria) = @_;
106              
107             # Go through each search critereon
108 0           for my $crit (@criteria)
109             {
110             # Look at each element of the critereon
111 0           my @subcrit = @$crit;
112 0           while(@subcrit)
113             {
114 0           my $col = shift @subcrit;
115              
116             # Did we find the clause?
117 0 0         if($col eq $clause)
118             {
119 0           return @subcrit;
120             }
121              
122             # Remove any parameters for a special-purpose clause
123 0   0       my $cnt = $self->critexceptions->{$col} || 0;
124 0           splice @subcrit, 0, $cnt;
125            
126             # Eat any following parameters
127 0 0         splice @subcrit, 0, $self->criteriamap->{$col}[1]
128             if $self->criteriamap->{$col};
129             }
130             }
131 0           return;
132             }
133              
134             # Create a where clause for use in searching
135             sub __create_where
136             {
137 0     0     my ($self, @criteria) = @_;
138              
139             # Keep track of where clause components and the data bound to it
140 0           my @clauses;
141             my @binds;
142              
143             # The perl to SQL name map
144 0           my %map = $self->_column_map;
145              
146             # Iterate over each critereon
147 0           for my $crit (@criteria)
148             {
149             # The components that make up this chunk of the where clause
150 0           my @sclause = ();
151              
152 0           my @subcrit = @$crit;
153 0           while(@subcrit)
154             {
155 0           my $col = shift @subcrit;
156 0           my $cnt = $self->critexceptions->{$col};
157              
158             # Is this a special-purpose clause?
159 0 0         if($cnt)
160             {
161 0           splice @subcrit, 0, $cnt;
162 0           next;
163             }
164            
165             # Get the operator for this column
166 0           my $op = shift @subcrit;
167 0           my $test;
168              
169             # Do we actually have a column with this name?
170 0 0 0       carp "No such column $col" and return unless $map{$col};
171              
172             # Is it a regular operator?
173 0 0         if(defined $self->criteriamap->{$op}[0])
174             {
175             # Does it take a parameter?
176 0 0         if($self->criteriamap->{$op}[1])
177             {
178 0           $test = "$map{$col} " . $self->criteriamap->{$op}[0] . ' ';
179 0           my $val = shift @subcrit;
180              
181             # Handle literal expressions
182 0 0         if(ref $val)
183             {
184 0           $test .= $$val;
185             }
186              
187             # Or a to-be-quoted value
188             else
189             {
190 0           $test .= '?';
191 0           push @binds, $val;
192             }
193             }
194             else # No parameter
195             {
196 0           $test = "$col " . $self->criteriamap->{$op}[0];
197             }
198             }
199             else # IN/NOT IN
200             {
201 0           $test = "$map{$col} " . $self->criteriamap->{$op}[1] . ' (';
202 0           my $val = shift @subcrit;
203              
204 0 0         if('SCALAR' eq reftype $val)
    0          
205             {
206             # It's a subselect, or other literalized expression
207 0           $test .= $$val;
208             }
209             elsif('ARRAY' eq reftype $val)
210             {
211             # It's an array of values
212 0           $test .= join(',', ('?')x@$val);
213 0           push @binds, @$val;
214             }
215             else
216             {
217             # Treat it like a single-element list
218 0           $test .= '?';
219 0           push @binds, $val;
220             }
221              
222 0           $test .= ')';
223             }
224 0           push @sclause, $test;
225             }
226              
227             # Stick the pieces together
228 0 0         push @clauses, join(' AND ', @sclause) if @sclause;
229             }
230              
231             # join the subclauses together
232             # Wrap them in parens if there are more than one
233 0 0         @clauses = map { "($_)" } @clauses if @clauses > 1;
  0            
234              
235             # Join them together
236 0   0       my $where = join(' OR ', @clauses) || '';
237            
238             # Add any required prefix
239 0           my $where_pre = $self->_where_prefix;
240 0 0         if($where_pre)
241             {
242 0 0         if($where)
243             {
244 0           $where = "($where_pre) AND ($where)";
245             }
246             }
247 0           return ($where, @binds);
248             }
249              
250             # Creates one or more items
251             # Does not return row objects
252             # Does not validate the input
253             # Returns an array of undef or 1 values (depending on reported success)
254             sub create_only
255             {
256 0     0 1   my ($self, @data) = @_;
257              
258             # parameter checking
259 0           my $handle = $self->handle;
260 0 0 0       carp "can't create without a database handle" and return unless $handle;
261 0 0 0       carp "can't create a row in a JOIN" and return if $self->_join_clause;
262              
263 0           my $table = $self->_sql_name;
264 0           my @cols = map { $_->{name}; } $self->_columns;
  0            
265 0           my %name2sql = $self->_column_map;
266 0           my @rv = ();
267              
268             # We really don't want any warnings...
269 0           local ($SIG{__WARN__}) = $self->w__noop;
270 0           for my $row (@data)
271             {
272             # Execute a statement per data item
273 0           my @use_cols = grep { exists $row->{$_}; } @cols;
  0            
274 0           my $sql = "INSERT INTO $table (" . join(',', @name2sql{@use_cols})
275             . ') VALUES (' . join(',', ('?') x @use_cols) . ')';
276              
277             # Get a statement handle
278 0           my $sth = $handle->prepare_cached($sql);
279 0 0 0       push @rv, undef and next unless $sth;
280              
281             # Execute and save the result
282 0           my $rc = $sth->execute(@{$row}{@use_cols});
  0            
283 0 0         push @rv, $rc?1:undef;
284             }
285 0           $handle->commit;
286 0           return @rv;
287             }
288              
289             # Creates multiple rows, returns the number of rows created (or
290             # whatever the handle object says is the number of rows)
291             sub bulk_create
292             {
293             # $cols_ref is an array of column (alias) names
294 0     0 1   my ($self, $cols_ref, @data) = @_;
295              
296 0           my $handle = $self->handle;
297 0 0 0       carp "can't create without a database handle" and return unless $handle;
298 0 0 0       carp "can't create a row in a JOIN" and return if $self->_join_clause;
299              
300 0           my $table = $self->table;
301 0 0 0       carp "can't create a row without a table" and return unless $table;
302 0           my @cols = map { $_->{name}; } $self->_columns;
  0            
303 0           my %name2sql = $self->_column_map;
304            
305 0           my @col_unk = grep { not exists $name2sql{$_} } @$cols_ref;
  0            
306              
307 0 0 0       warn "Unknown columns '" . join("', '", @col_unk) . "'" and return
308             if @col_unk;
309              
310             # Map unique keys to avoid duplicates
311 0           my @uniqs_map;
312 0           for my $un ($self->_unique_keys)
313             {
314 0           my $h = { };
315 0           for my $i (0..$#$cols_ref)
316             {
317 0 0         if(grep { $name2sql{$cols_ref->[$i]} eq $_ } @$un)
  0            
318             {
319 0           $h->{$name2sql{$cols_ref->[$i]}} = $i;
320             }
321             }
322 0 0         push @uniqs_map, $h if %$h;
323             }
324              
325 0           my $sql = "INSERT INTO $table (" . join(',', @name2sql{@$cols_ref}) . ') ';
326              
327             # We build the complete insert statement from a bunch of select statements
328             # pasted together with UNION ALL
329             # To avoid errors, we use another select to make sure the row is unique
330 0           my @selects;
331 0           for my $d (@data)
332             {
333 0           my $sel = 'SELECT ' . join(',', map { $handle->quote($_) } @$d)
  0            
334             . ' FROM DUAL';
335 0 0         if(@uniqs_map)
336             {
337 0           my @wherefrag = ();
338 0           for my $un (@uniqs_map)
339             {
340 0           my @wk = map { "$_=" . $handle->quote($d->[$un->{$_}]) }
  0            
341             keys %$un;
342 0           push @wherefrag, join(' AND ', @wk);
343             }
344 0           $sel .= " WHERE NOT EXISTS (SELECT 1 FROM $table WHERE (" .
345             join(') OR (', @wherefrag) . '))';
346             }
347 0           push @selects, $sel;
348             }
349              
350 0           $sql .= join(' UNION ALL ', @selects);
351 0           my $res = $handle->do($sql);
352              
353             # We don't need warnings about commit being ineffective
354 0           local ($SIG{__WARN__}) = $self->w__noop;
355 0           $handle->commit;
356 0           return $res;
357             }
358              
359             # Check parameters against the declared constraints and create
360             # a row in a table, returning the corresponding row object.
361             sub create
362             {
363 0     0 1   my ($self, %params) = @_;
364 0           my $handle = $self->handle;
365 0 0 0       carp "can't create without a database handle" and return unless $handle;
366 0 0 0       carp "can't create a row in a JOIN" and return if $self->_join_clause;
367              
368             # Get the data we'd need to do the create
369 0           my ($flag, $keys, $values, $npk, @binds) =
370             $self->__check_constraints($self, %params);
371 0 0         return unless $flag;
372              
373             # Generate the SQL command
374 0           my $sql = 'INSERT INTO ' . $self->_table . " ($keys) VALUES ($values)";
375              
376             # Run the command
377 0 0         unshift @binds, undef if @binds; # Avoid DBI breakage
378 0           my $res = $handle->do($sql, @binds);
379 0 0 0       carp "Database error: ", $handle->errstr and return unless $res;
380              
381             # Get return information
382 0           my @res;
383              
384             # Handle the case where the primary key is null
385 0 0         if($npk)
    0          
386             {
387 0           my $np = $self->_select_null_primary;
388 0 0         if($np)
389             {
390 0           my $data = $handle->selectall_arrayref($np);
391 0 0 0       if(not $data or not defined $data->[0][0])
392             {
393 0           carp "Database error: ", $handle->errstr;
394 0           $self->__do_rollback;
395 0           return;
396             }
397 0           @res = $self->search([
398 0           map {($_, 'eq', $data->[0][0])} $self->_primary_key]);
399             }
400             }
401              
402             # Handle defined unique keys
403             elsif($self->_unique_keys)
404             {
405 0           my ($un) = $self->_unique_keys;
406 0           my @pk = @$un;
407            
408             # We search by the first unique key we find
409 0 0         @res = $self->search([
410 0           map {($_, (defined $params{$_}?(eq => $params{$_}):('isnull'))) }
411             @pk ]);
412             }
413              
414             # No unique key - do it based on everything we've got in params
415             else
416             {
417             # This does a search on all passed-in parameters
418 0 0         @res = $self->search([
419 0           map {($_, (defined $params{$_}?(eq => $params{$_}):('isnull'))) }
420 0           grep { exists $params{$_} }
421             keys %params ]);
422             }
423              
424             # Make sure we have exactly one row returned...
425 0 0         if(not @res)
426             {
427 0           carp $self->E_NOROWSFOUND;
428 0           $self->__do_rollback;
429 0           return;
430             }
431 0 0         if(@res > 1)
432             {
433 0           carp $self->E_TOOMANYROWS;
434 0           $self->__do_rollback;
435 0           return;
436             }
437              
438             # Turn off warnings and commit
439 0           local ($SIG{__WARN__}) = $self->w__noop;
440 0           $handle->commit;
441 0           return $res[0];
442             }
443              
444             # Delete stuff from the database
445             sub delete
446             {
447 0     0 1   my ($self, @criteria) = @_;
448 0           my $handle = $self->handle;
449 0 0 0       carp "can't delete without a database handle" and return unless $handle;
450 0 0 0       carp "can't delete from a JOIN" and return if $self->_join_clause;
451              
452             # Create the SQL command
453 0           my ($where, @binds) = $self->__create_where(@criteria);
454 0           my $sql = "DELETE FROM " . $self->_sql_name;
455 0 0         $sql .= " WHERE $where" if $where;
456              
457 0 0         unshift @binds, undef if @binds; # Handle DBI lossage
458 0           my $res = $handle->do($sql, @binds);
459            
460             # Report errors
461 0 0         if(not $res)
462             {
463 0           carp "Database error " . $handle->errstr;
464 0           $self->__do_rollback;
465 0           return;
466             }
467              
468             # Commit and return
469 0           local ($SIG{__WARN__}) = $self->w__noop;
470 0           $handle->commit;
471 0           return $self;
472             }
473              
474             # Search the database, return a row object per returned item
475             sub search
476             {
477 0     0 1   my ($self, @criteria) = @_;
478 0           my $handle = $self->handle;
479 0 0 0       carp "can't search without a database handle" and return unless $handle;
480              
481             # create the base select statement
482 0           my $sql = 'SELECT ' . join(',', $self->_column_sql_names) . ' FROM '
483             . $self->_sql_name;
484              
485             # Add any join clause
486 0           my $join = $self->_join_clause;
487 0 0         $sql .= " $join" if $join;
488              
489             # Add a where clause, if necessary
490 0           my ($where, @binds) = $self->__create_where(@criteria);
491 0 0         $sql .= " WHERE $where" if $where;
492              
493             # Add any GROUP BY clause
494 0           my @g = $self->_group_by;
495 0 0         $sql .= " GROUP BY " . join(',',@g) if @g;
496              
497             # Add any ORDER BY clause
498 0           my $ord = $self->__find_orderby(@criteria);
499 0 0         $sql .= " $ord" if $ord;
500              
501             # Add any LIMIT clause
502 0           my $limit = $self->__find_limit(@criteria);
503 0 0         $sql .= " $limit" if $limit;
504            
505 0 0         unshift @binds, undef if @binds;
506 0           my $data = $handle->selectall_arrayref($sql, @binds);
507              
508 0 0 0       carp "Database error " . $handle->errstr and return unless $data;
509              
510             # The return values row class
511 0           my $rclass = $self->_row_class;
512 0 0         $rclass = ref $self if $self->isa($rclass);
513              
514             # Create the return values
515 0           my @res;
516 0           for my $row (@$data)
517             {
518 0           my $robj = bless $self->new, $rclass;
519 0           $robj->__set_data(@$row);
520              
521             # Add the where clause, so we can find this row later
522 0           $robj->__create_where;
523 0           push @res, $robj;
524             }
525 0           return @res;
526             }
527              
528             # Return the number of rows a query would return
529             sub size
530             {
531 0     0 1   my ($self, @criteria) = @_;
532 0           my $handle = $self->handle;
533 0 0 0       carp "can't find table size without a database handle" and return
534             unless $handle;
535              
536             # Create the base SQL statement
537 0           my $table = $self->_sql_name;
538 0           my $sql = "SELECT COUNT(*) FROM $table";
539              
540             # Add any GROUP BY clause
541 0           my @g = $self->_group_by;
542 0 0         $sql .= " GROUP BY " . join(',',@g) if @g;
543              
544             # Add any join clause
545 0           my $join = $self->_join_clause;
546 0 0         $sql .= " $join" if $join;
547              
548 0           my ($where, @binds) = $self->__create_where(@criteria);
549 0 0         $sql .= " WHERE $where" if $where;
550            
551 0 0         unshift @binds, undef if @binds; # Avoid DBI lossage
552 0           my $data = $handle->selectall_arrayref($sql, @binds);
553              
554 0 0 0       carp "Database error " . $handle->errstr and return unless $data;
555 0           return $data->[0][0];
556             }
557              
558             1;
559              
560             __END__