File Coverage

lib/File/Properties/Database.pm
Criterion Covered Total %
statement 189 221 85.5
branch 51 80 63.7
condition 21 51 41.1
subroutine 26 30 86.6
pod 13 14 92.8
total 300 396 75.7


line stmt bran cond sub pod time code
1             # ----------------------------------------------------------------------------
2             #
3             # This module provides an interface to the SQLite database used to
4             # cache data for File::Properties modules.
5             #
6             # Copyright © 2010,2011 Brendt Wohlberg
7             # See distribution LICENSE file for license details.
8             #
9             # Most recent modification: 5 November 2011
10             #
11             # ----------------------------------------------------------------------------
12              
13             package File::Properties::Database;
14             our $VERSION = 0.01;
15              
16 5     5   37076 use File::Properties::Error;
  5         11  
  5         35  
17              
18             require 5.005;
19 5     5   295 use strict;
  5         36  
  5         182  
20 5     5   232 use warnings;
  5         11  
  5         200  
21 5     5   19994 use DBI qw(:sql_types);
  5         187872  
  5         5000  
22 5     5   10282 use DBD::SQLite;
  5         109865  
  5         1920  
23 5     5   75 use Error qw(:try);
  5         13  
  5         50  
24              
25              
26             # ----------------------------------------------------------------------------
27             # Constructor
28             # ----------------------------------------------------------------------------
29             sub new {
30 3     3 1 895 my $clss = shift;
31              
32 3         53 my $self = {};
33 3         11 bless $self, $clss;
34 3         21 $self->_init(@_);
35 3         36 return $self;
36             }
37              
38              
39             # ----------------------------------------------------------------------------
40             # Initialiser
41             # ----------------------------------------------------------------------------
42             sub _init {
43 3     3   7 my $self = shift;
44 3         6 my $dbfp = shift; # Database file path
45 3         7 my $opts = shift; # Options hash
46              
47 3 50       15 $opts = {} if undef $opts;
48             # Throw exception if DB file $dpfp does not exist and 'NoCreate'
49             # option is true.
50 3 50 0     23 throw File::Properties::Error("DBI file $dbfp not found")
      33        
51             if $opts->{'NoCreate'} and (not defined $dbfp or not -f $dbfp);
52             # If DB file not specified, construct it in memory
53 3 50       13 $dbfp = ':memory:' if (not defined $dbfp);
54             ## Throw exception if error encountered opening DBI connection
55 3 50       24 my $dbh = _dbopen($dbfp, $opts->{'ReadOnly'}?1:0);
56 3 50       16 throw File::Properties::Error("Error opening DBI interface to file $dbfp")
57             if not defined($dbh);
58             # Record constructor options
59 3         27 $self->opts($opts);
60             # Record SQLite DBI interface
61 3         20 $self->dbi($dbh);
62             # Table column specifications
63 3         36 $self->definedcolumns(undef,{});
64             }
65              
66              
67             # ----------------------------------------------------------------------------
68             # Destructor
69             # ----------------------------------------------------------------------------
70             sub DESTROY {
71 3     3   16903 my $self = shift;
72              
73 3         18 _dbclose($self->dbi);
74             }
75              
76              
77             # ----------------------------------------------------------------------------
78             # Get (or set) options specified at initialisation
79             # ----------------------------------------------------------------------------
80             sub opts {
81 3     3 1 7 my $self = shift;
82              
83 3 50       28 $self->{'opts'} = shift if (@_);
84 3         10 return $self->{'opts'}
85             }
86              
87              
88             # ----------------------------------------------------------------------------
89             # Get (or set) dbi handle
90             # ----------------------------------------------------------------------------
91             sub dbi {
92 86     86 1 191 my $self = shift;
93              
94 86 100       332 $self->{'dbih'} = shift if (@_);
95 86         682785 return $self->{'dbih'}
96             }
97              
98              
99             # ----------------------------------------------------------------------------
100             # Get (or set) table column specification hash or hash entry
101             # ----------------------------------------------------------------------------
102             sub definedcolumns {
103 30     30 1 58 my $self = shift;
104 30         2621 my $tbnm = shift; # Table name
105              
106             ## If table name is defined, additional parameter specifies new
107             ## value for that hash entry, otherwise the additional parameter
108             ## specifies new value for entire hash
109 30 100       103 if (@_) {
110 9 100       35 if (defined $tbnm) {
111 6         30 $self->{'tblc'}->{$tbnm} = shift;
112             } else {
113 3         26 $self->{'tblc'} = shift;
114             }
115             }
116              
117             ## If table name is specified and the corresponding entry is not
118             ## defined, obtain the column names from the SQL database
119 30 50 66     568 if (defined $tbnm and not defined $self->{'tblc'}->{$tbnm}) {
120 0         0 $self->{'tblc'}->{$tbnm} = $self->columns($tbnm);
121             }
122              
123             # Return hash reference, or hash entry if table name specified
124 30 100       125 return (defined $tbnm)? $self->{'tblc'}->{$tbnm}:$self->{'tblc'};
125             }
126              
127              
128             # ----------------------------------------------------------------------------
129             # Execute SQL command
130             # ----------------------------------------------------------------------------
131             sub sql {
132 9     9 1 17 my $self = shift;
133 9         16 my $sqlc = shift; # SQL command text
134              
135 9         78 return $self->dbi->do($sqlc);
136             }
137              
138              
139             # ----------------------------------------------------------------------------
140             # Define (and initialise) table
141             # ----------------------------------------------------------------------------
142             sub definetable {
143 6     6 1 14 my $self = shift;
144 6         12 my $tbnm = shift; # Table name
145 6         11 my $cols = shift; # Column specification
146              
147             ## Create table if it doesn't exist. WARNING: when the table already
148             ## exists, there is currently not a test to ensure that the existing
149             ## layout matches the column specification in the arguments to this
150             ## method
151 6         44 my $sqlc = "CREATE TABLE IF NOT EXISTS $tbnm (" . join(',',@$cols) . ');';
152 6         35 my $drtv = $self->sql($sqlc);
153             # Record column names for this table
154 6         680872 $self->definedcolumns($tbnm, [map { /^[^\s]+/; $& } @$cols]);
  25         96  
  25         140  
155 6         39 return $drtv;
156             }
157              
158              
159             # ----------------------------------------------------------------------------
160             # Insert rows into table
161             # ----------------------------------------------------------------------------
162             sub insert {
163 14     14 1 39 my $self = shift;
164 14         28 my $tbnm = shift; # Table name
165 14         26 my $opts = shift; # Insert options
166              
167             # Determine column names for insert
168 14         71 my $clnm = $self->_optioncols($tbnm, $opts);
169             # Determine data for insert
170 14         52 my $data = _optiondata($tbnm, $clnm, $opts);
171             # Start transaction (autocommit off)
172 14 50       59 $self->dbi->begin_work or
173             throw File::Properties::Error("DBI error ".$self->dbi->errstr);
174             # Construct string describing columns corresponding to row data,
175             # using either specified array of column names, or recorded column
176             # names for this table
177 14         398 my $clst = join(',',@$clnm);
178             # Construct insert statement
179 44         114 my $sqlc = "INSERT INTO $tbnm ($clst) VALUES (" .
180 14         63 join(',', map { '?' } @$clnm) . ');';
181             # Prepare for insertion
182 14         41 my $sth = $self->dbi->prepare($sqlc);
183             # Execute insertion
184 14         2175 return $self->_executedata($sth, $data);
185             }
186              
187              
188             # ----------------------------------------------------------------------------
189             # Update rows in table
190             # ----------------------------------------------------------------------------
191             sub update {
192 1     1 1 1 my $self = shift;
193 1         2 my $tbnm = shift; # Table name
194 1         2 my $opts = shift; # Update options
195              
196             # Determine column names for update
197 1         4 my $clnm = $self->_optioncols($tbnm, $opts);
198             # Determine data for update
199 1         3 my $data = _optiondata($tbnm, $clnm, $opts);
200             # Start transaction (autocommit off)
201 1 50       3 $self->dbi->begin_work or
202             throw File::Properties::Error("DBI error ".$self->dbi->errstr);
203             # Construct update statement
204 1         23 my $sqlc = "UPDATE $tbnm SET " . join(',', map { "$_=?" } @$clnm);
  1         5  
205 1         4 $sqlc .= _optionwhere($opts);
206             # Prepare for update
207 1         3 my $sth = $self->dbi->prepare($sqlc);
208             # Execute update
209 1         81 return $self->_executedata($sth, $data);
210             }
211              
212              
213             # ----------------------------------------------------------------------------
214             # Select data from table
215             # ----------------------------------------------------------------------------
216             sub retrieve {
217 19     19 1 10138 my $self = shift;
218 19         36 my $tbnm = shift; # Table name
219 19         26 my $opts = shift; # Select options
220              
221 19         37 my $slc = 'SELECT ';
222             # Select statement includes DISTINCT if 'Distint' option true
223 19 50 33     168 $slc .= 'DISTINCT ' if (ref($opts) eq 'HASH' and $opts->{'Distinct'});
224 19         28 my ($ncl, $cln);
225             ## List of returned columns is constructed from the array provided
226             ## with the 'Columns' option, otherwise all columns are returned
227 19 50 33     595 if (ref($opts) eq 'HASH' and ref($opts->{'Columns'}) eq 'ARRAY') {
228 0         0 $slc .= join(',',@{$opts->{'Columns'}}) . ' ';
  0         0  
229 0         0 $cln = $opts->{'Columns'};
230 0         0 $ncl = scalar @$cln;
231             } else {
232 19         46 $slc .= '* ';
233 19         80 $cln = $self->definedcolumns($tbnm);
234 19         41 $ncl = scalar @$cln;
235             }
236             # Append FROM and WHERE clauses to select statement
237 19         101 $slc .= "FROM $tbnm " . _optionwhere($opts);
238             # Append optional additional clauses to select statement
239 19 50 33     1202 $slc .= $opts->{'Suffix'} if (ref($opts) eq 'HASH' and $opts->{'Suffix'});
240             # Check option 'ReturnType' for invalid values
241 19 50 33     402 throw File::Properties::Error("Option 'ReturnType' may only have".
      66        
      33        
242             "values 'Array' or 'Hash'")
243             if (ref($opts) eq 'HASH' and defined($opts->{'ReturnType'}) and
244             not($opts->{'ReturnType'} eq 'Array' or
245             $opts->{'ReturnType'} eq 'Hash'));
246             ## DBI method for retrieving data depends on options 'ReturnType'
247             ## and 'FirstRow'. If 'ReturnType' is not specified, data is
248             ## retrieved as an array. If 'FirstRow' option is unspecified, or is
249             ## false, a single row is returned.
250 19         35 my $dat = undef;
251 19 100 33     161 if (ref($opts) eq 'HASH' and defined($opts->{'ReturnType'}) and
      66        
252             $opts->{'ReturnType'} eq 'Hash') {
253 10 100 66     110 if (ref($opts) eq 'HASH' and $opts->{'FirstRow'}) {
254             # Retrieve single row as a hash indexed by column names
255 9         44 $dat = $self->dbi->selectrow_hashref($slc);
256             } else {
257             # Retrieve rows as an array of arrays
258 1         4 my $adt = $self->dbi->selectall_arrayref($slc);
259             ## Map array to hash of arrays indexed by column names
260 1         193 $dat = {};
261 1         3 map { $dat->{$_} = [] } @$cln;
  2         6  
262 1         2 map { my $r = $_; map { push @{$dat->{$cln->[$_]}}, $r->[$_] }
  2         3  
  4         5  
  4         13  
  2         6  
263 2         3 @{[0 .. (scalar @$cln-1)]} } @$adt;
264             }
265             } else {
266 9 100 66     65 if (ref($opts) eq 'HASH' and $opts->{'FirstRow'}) {
267             # Retrieve single row as an array
268 7         37 $dat = $self->dbi->selectrow_arrayref($slc);
269             } else {
270             # Retrieve rows as an array of arrays
271 2         7 $dat = $self->dbi->selectall_arrayref($slc);
272             }
273             }
274              
275 19         6738 return $dat;
276             }
277              
278              
279             # ----------------------------------------------------------------------------
280             # Remove data from table
281             # ----------------------------------------------------------------------------
282             sub remove {
283 1     1 1 2 my $self = shift;
284 1         3 my $tbnm = shift; # Table name
285 1         1 my $opts = shift; # Delete options
286              
287             # Throw exception if options hash includes neither a 'Where'
288             # constraint nor the 'RemoveAll' flag
289 1 50 33     16 throw File::Properties::Error("Method remove called without row selection")
      33        
290             if (ref($opts) ne 'HASH' or (not defined $opts->{'Where'}
291             #and not defined $opts->{'Suffix'}
292             and not $opts->{'RemoveAll'}));
293             # Set up SQL DELETE statement
294 1         4 my $sqlc = "DELETE FROM $tbnm "._optionwhere($opts);
295             ## Append optional additional clauses to delete statement
296             #$sqlc .= $opts->{'Suffix'} if (ref($opts) eq 'HASH' and $opts->{'Suffix'});
297             # Execute SQL DELETE statement
298 1         5 return $self->sql($sqlc);
299             }
300              
301              
302             # ----------------------------------------------------------------------------
303             # Determine names of all tables
304             # ----------------------------------------------------------------------------
305             sub tables {
306 0     0 1 0 my $self = shift;
307              
308             # Set up SQL statement
309 0         0 my $sqlc = "SELECT * FROM sqlite_master WHERE type = 'table'";
310             # Execute SQL statement
311 0         0 my $rar = $self->dbi->selectall_arrayref($sqlc);
312 0         0 return [map { $_->[1] } @$rar];
  0         0  
313             }
314              
315              
316             # ----------------------------------------------------------------------------
317             # Determine names of columns in specified table
318             # ----------------------------------------------------------------------------
319             sub columns {
320 0     0 1 0 my $self = shift;
321 0         0 my $tbnm = shift; # Table name
322              
323             # Set up SQL statement
324 0         0 my $sqlc = "PRAGMA table_info($tbnm)";
325             # Execute SQL statement
326 0         0 my $rar = $self->dbi->selectall_arrayref($sqlc);
327 0         0 return [map { $_->[1] } @$rar];
  0         0  
328             }
329              
330              
331             # ----------------------------------------------------------------------------
332             # Determine number of rows in specified table
333             # ----------------------------------------------------------------------------
334             sub numrows {
335 0     0 0 0 my $self = shift;
336 0         0 my $tbnm = shift; # Table name
337              
338             # Set up SQL statement
339 0         0 my $sqlc = "SELECT Count(*) FROM $tbnm";
340             # Execute SQL statement
341 0         0 my $rar = $self->dbi->selectall_arrayref($sqlc);
342 0         0 return $rar->[0]->[0];
343             }
344              
345              
346             # ----------------------------------------------------------------------------
347             # Determine whether table exists
348             # ----------------------------------------------------------------------------
349             sub tableexists {
350 7     7 1 15 my $self = shift;
351 7         16 my $tbnm = shift; # Table name
352              
353 7         28 return _tableexists($self->dbi, $tbnm);
354             }
355              
356              
357             # ----------------------------------------------------------------------------
358             # Create column name array for insert and update operations
359             # ----------------------------------------------------------------------------
360             sub _optioncols {
361 15     15   28 my $self = shift;
362 15         25 my $tbnm = shift; # Table name
363 15         29 my $opts = shift; # Options hash
364              
365             ## Column names are taken from keys of the 'Data' option if it is a
366             ## hash, otherwise from 'Columns' option if provided, otherwise
367             ## assumed to be all columns in table order
368 15         27 my $clnm;
369 15 50       51 if (ref($opts) eq 'HASH') {
370 15 100       183 if (ref($opts->{'Data'}) eq 'HASH') {
    100          
371 12         22 $clnm = [sort keys %{$opts->{'Data'}}];
  12         133  
372             } elsif (ref($opts->{'Columns'}) eq 'ARRAY') {
373 1         3 $clnm = $opts->{'Columns'};
374             } else {
375 2         11 $clnm = $self->definedcolumns($tbnm);
376             }
377             } else {
378 0         0 $clnm = $self->definedcolumns($tbnm);
379             }
380              
381 15         44 return $clnm;
382             }
383              
384              
385             # ----------------------------------------------------------------------------
386             # Create data array for insert and update operations
387             # ----------------------------------------------------------------------------
388             sub _optiondata {
389 15     15   38 my $tbnm = shift; # Table name
390 15         30 my $clnm = shift; # Column names
391 15         26 my $opts = shift; # Options hash
392              
393             ## If 'Data' option provided, process it into a form ready for
394             ## insertion
395 15         37 my $data = undef;
396 15 50 33     216 if (ref($opts) eq 'HASH' and ref($opts->{'Data'})) {
397             # Data is provided as a hash
398 15 100       65 if (ref($opts->{'Data'}) eq 'HASH') {
    50          
399             # If first column in hash is an array reference, assume all
400             # columns are array references, specifying multiple insertion
401             # rows
402 12 100       57 if (ref($opts->{'Data'}->{$clnm->[0]}) eq 'ARRAY') {
403 1         2 $data = [];
404 1         2 my $nrow = scalar @{$opts->{'Data'}->{$clnm->[0]}};
  1         73  
405 1         6 for (my $n = 0; $n < $nrow; $n++) {
406 4         6 push @$data, [map { $opts->{'Data'}->{$_}->[$n] } @$clnm];
  8         27  
407             }
408             }
409             # Assume that only a single row is to be inserted
410             else {
411 11         38 $data = [[map { $opts->{'Data'}->{$_} } @$clnm]];
  37         114  
412             }
413             }
414             # Data is provided as an array
415             elsif (ref($opts->{'Data'}) eq 'ARRAY') {
416             # If the first entry in the array is an array reference, assume
417             # multiple insertion rows are provided
418 3 100       12 if (ref($opts->{'Data'}->[0]) eq 'ARRAY') {
419 1         5 $data = $opts->{'Data'};
420             }
421             # Assume that only a single row is to be inserted
422             else {
423 2         4 $data = [[@{$opts->{'Data'}}]];
  2         10  
424             }
425             } else {
426 0         0 throw File::Properties::Error("Data option must be a hash or an array");
427             }
428             }
429              
430 15         45 return $data;
431             }
432              
433              
434             # ----------------------------------------------------------------------------
435             # Create where statement for retrieve and remove operations
436             # ----------------------------------------------------------------------------
437             sub _optionwhere {
438 21     21   730 my $opts = shift; # Options hash
439              
440 21         38 my $whr = '';
441             ## If option 'Where' is a hash reference, key/value pairs are
442             ## assumed to correspond to column name/value pairs in a conjunction
443             ## of equality constraints in a WHERE clause. If it is not a hash
444             ## reference, the value is assume to be a string containing the
445             ## WHERE clause.
446 21 50 33     553 if (ref($opts) eq 'HASH' and defined $opts->{'Where'}) {
447 21 100       243 if (ref($opts->{'Where'}) eq 'HASH') {
448 43         726 $whr .= " WHERE " . join ' AND ',
449 19         48 map { "$_='$opts->{'Where'}->{$_}'" } keys %{$opts->{'Where'}};
  19         95  
450             } else {
451 2         7 $whr .= " WHERE " . $opts->{'Where'};
452             }
453             }
454              
455 21         257 return $whr;
456             }
457              
458              
459             # ----------------------------------------------------------------------------
460             # Execute statement handle object over specified data
461             # ----------------------------------------------------------------------------
462             sub _executedata {
463 15     15   32 my $self = shift;
464 15         26 my $sth = shift; # Statement handle object
465 15         23 my $data = shift; # Data array
466              
467             ## If data provided, insert each row and then commit,
468             ## otherwise return the statement handle object from prepare
469 15 50       47 if (defined $data) {
470 15         21 my $row;
471 15         44 foreach $row (@$data) {
472 20 50       8255 $sth->execute(@$row) or
473             throw File::Properties::Error("DBI error ".$self->dbi->errstr);
474             }
475 15 50       68 if (not $self->dbi->commit) {
476 0         0 my $err = $self->dbi->errstr;
477 0         0 $self->dbi->rollback;
478 0         0 throw File::Properties::Error("DBI error $err");
479             }
480 15         913 return $data;
481             } else {
482 0         0 return $sth;
483             }
484             }
485              
486              
487             # ----------------------------------------------------------------------------
488             # Connect to SQLite database
489             # ----------------------------------------------------------------------------
490             sub _dbopen {
491 3     3   8 my $dbf = shift; # DB file path
492 3         6 my $rof = shift; # Read only flag
493              
494 0     0   0 my $dbh = DBI->connect("dbi:SQLite:dbname=$dbf",'','',
495             { ReadOnly => $rof,
496             AutoCommit => 1,
497             PrintError => 0,
498             RaiseError => 1,
499             HandleError =>
500 3         67 sub {throw File::Properties::Error($_[0])} });
501 3 50       6306 $dbh->{sqlite_unicode} = 1 if defined $dbh;
502 3         11 return $dbh;
503             }
504              
505              
506             # ----------------------------------------------------------------------------
507             # Disconnect from SQLite database
508             # ----------------------------------------------------------------------------
509             sub _dbclose {
510 3     3   8 my $dbh = shift; # DBI handle
511              
512 3         513 $dbh->disconnect();
513             }
514              
515              
516             # ----------------------------------------------------------------------------
517             # Determine whether specified table exists in database
518             # ----------------------------------------------------------------------------
519             sub _tableexists {
520 7     7   13 my $dbs = shift; # DBI handle or DB file path
521 7         14 my $tbl = shift; # Table name
522              
523 7         10 my $dbh;
524             ## If $dbs is a DBI handle, use that handle for database access,
525             ## otherwise assume it is the path to an SQLite DB file and attempt
526             ## to open it.
527 7 50       28 if (ref($dbs) eq 'DBI::db') {
528 7         15 $dbh = $dbs;
529             } else {
530 0         0 my $dbf = $dbs;
531 0 0       0 return 0 if not -f $dbf;
532 0         0 $dbh = DBI->connect("dbi:SQLite:$dbf",'','', {PrintError => 0,
533             RaiseError => 0});
534 0 0       0 return 0 if not defined $dbh;
535             }
536             ## Determine whether named table exists
537 7         30 my $slc = "SELECT NAME FROM sqlite_master WHERE TYPE='table' ".
538             "AND NAME='$tbl'";
539 7         97 my $a = $dbh->selectrow_arrayref($slc);
540             # Close database connection if it was opened in this function
541 7 50       2605 $dbh->disconnect() if not ref($dbs) eq 'DBI::db';
542 7 50       61 return (defined $a)?(@$a > 0):0;
543             }
544              
545              
546             # ----------------------------------------------------------------------------
547             # End of method definitions
548             # ----------------------------------------------------------------------------
549              
550              
551             1;
552             __END__