File Coverage

blib/lib/Genealogy/ObituaryDailyTimes/DB.pm
Criterion Covered Total %
statement 32 314 10.1
branch 1 180 0.5
condition 6 85 7.0
subroutine 10 20 50.0
pod 2 8 25.0
total 51 607 8.4


line stmt bran cond sub pod time code
1             package Genealogy::ObituaryDailyTimes::DB;
2              
3             =head1 NAME
4              
5             Genealogy::ObituaryDailyTimes::DB
6              
7             =cut
8              
9             # Author Nigel Horne: njh@bandsman.co.uk
10             # Copyright (C) 2015-2023, Nigel Horne
11              
12             # Usage is subject to licence terms.
13             # The licence terms of this software are as follows:
14             # Personal single user, single computer use: GPL2
15             # All other users (including Commercial, Charity, Educational, Government)
16             # must apply in writing for a licence for use from Nigel Horne at the
17             # above e-mail.
18              
19             # Abstract class giving read-only access to CSV, XML and SQLite databases via Perl without writing any SQL.
20             # Look for databases in $directory in this order;
21             # SQLite (file ends with .sql)
22             # PSV (pipe separated file, file ends with .psv)
23             # CSV (file ends with .csv or .db, can be gzipped)
24             # XML (file ends with .xml)
25              
26             # For example, you can access the files in /var/db/foo.csv via this class:
27              
28             # package MyPackageName::DB::foo;
29              
30             # use NJH::Snippets::DB;
31              
32             # our @ISA = ('NJH::Snippets::DB');
33              
34             # 1;
35              
36             # You can then access the data using:
37             # my $foo = MyPackageName::DB::foo->new(directory => '/var/db');
38             # my $row = $foo->fetchrow_hashref(customer_id => '12345);
39             # print Data::Dumper->new([$row])->Dump();
40              
41             # CSV files can have empty lines of comment lines starting with '#', to make them more readable
42              
43             # If the table has a column called "entry", sorts are based on that
44             # To turn that off, pass 'no_entry' to the constructor, for legacy
45             # reasons it's enabled by default
46             # TODO: Switch that to off by default, and enable by passing 'entry'
47              
48             # TODO: support a directory hierarchy of databases
49             # TODO: consider returning an object or array of objects, rather than hashes
50             # TODO: Add redis database - could be of use for Geo::Coder::Free
51             # use select() to select a database - use the table arg
52             # new(database => 'redis://servername');
53              
54 4     4   28 use warnings;
  4         8  
  4         142  
55 4     4   20 use strict;
  4         8  
  4         100  
56              
57 4     4   2125 use DBD::SQLite::Constants qw/:file_open/; # For SQLITE_OPEN_READONLY
  4         128277  
  4         996  
58 4     4   46 use File::Basename;
  4         9  
  4         336  
59 4     4   34 use File::Spec;
  4         8  
  4         155  
60 4     4   1809 use File::pfopen 0.02;
  4         2252  
  4         191  
61 4     4   3602 use File::Temp;
  4         79281  
  4         363  
62 4     4   1928 use Error::Simple;
  4         17931  
  4         39  
63 4     4   201 use Carp;
  4         10  
  4         14423  
64              
65             our $directory;
66             our $logger;
67             our $cache;
68             our $cache_duration;
69              
70             =head1 SUBROUTINES/METHODS
71              
72             =head2 init
73              
74             Set some class level defaults.
75              
76             __PACKAGE__::DB::init(directory => '../databases')
77              
78             See the documentation for new() to see what variables can be set
79              
80             =cut
81              
82             sub init {
83 4 50   4 1 28 my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0         0  
84              
85 4   66     21 $directory ||= $args{'directory'};
86 4   33     20 $logger ||= $args{'logger'};
87 4   33     18 $cache ||= $args{'cache'};
88 4   66     25 $cache_duration ||= $args{'cache_duration'};
89             }
90              
91             =head2 new
92              
93             Create an object to point to a read-only database.
94              
95             Arguments:
96              
97             cache => place to store results
98             cache_duration => how long to store results in the cache (default is 1 hour)
99             directory => where the database file is held
100              
101             If the arguments are not set, tries to take from class level defaults
102              
103             =cut
104              
105             sub new {
106 0     0 1   my $proto = shift;
107 0 0         my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0            
108              
109 0   0       my $class = ref($proto) || $proto;
110              
111 0 0         if($class eq __PACKAGE__) {
112 0           croak("$class: abstract class");
113             }
114              
115 0 0 0       croak("$class: where are the files?") unless($directory || $args{'directory'});
116             # init(\%args);
117              
118             return bless {
119             logger => $args{'logger'} || $logger,
120             directory => $args{'directory'} || $directory, # The directory containing the tables in XML, SQLite or CSV format
121             cache => $args{'cache'} || $cache,
122             cache_duration => $args{'cache_duration'} || $cache_duration || '1 hour',
123             table => $args{'table'}, # The name of the file containing the table, defaults to the class name
124 0   0       no_entry => $args{'no_entry'} || 0,
      0        
      0        
      0        
      0        
125             }, $class;
126             }
127              
128             sub set_logger {
129 0     0 0   my $self = shift;
130              
131 0           my %args;
132              
133 0 0         if(ref($_[0]) eq 'HASH') {
    0          
    0          
134 0           %args = %{$_[0]};
  0            
135             } elsif(!ref($_[0])) {
136 0           Carp::croak('Usage: set_logger(logger => $logger)');
137             } elsif(scalar(@_) % 2 == 0) {
138 0           %args = @_;
139             } else {
140 0           $args{'logger'} = shift;
141             }
142              
143 0           $self->{'logger'} = $args{'logger'};
144              
145 0           return $self;
146             }
147              
148             # Open the database.
149              
150             sub _open {
151 0     0     my $self = shift;
152             my %args = (
153             sep_char => '!',
154 0 0         ((ref($_[0]) eq 'HASH') ? %{$_[0]} : @_)
  0            
155             );
156              
157 0   0       my $table = $self->{'table'} || ref($self);
158 0           $table =~ s/.*:://;
159              
160 0 0         if($self->{'logger'}) {
161 0           $self->{'logger'}->trace("_open $table");
162             }
163 0 0         return if($self->{$table});
164              
165             # Read in the database
166 0           my $dbh;
167              
168 0   0       my $dir = $self->{'directory'} || $directory;
169 0           my $slurp_file = File::Spec->catfile($dir, "$table.sql");
170 0 0         if($self->{'logger'}) {
171 0           $self->{'logger'}->debug("_open: try to open $slurp_file");
172             }
173              
174 0 0         if(-r $slurp_file) {
175 0           require DBI;
176              
177 0           DBI->import();
178              
179 0           $dbh = DBI->connect("dbi:SQLite:dbname=$slurp_file", undef, undef, {
180             sqlite_open_flags => SQLITE_OPEN_READONLY,
181             });
182 0           $dbh->do('PRAGMA synchronous = OFF');
183 0           $dbh->do('PRAGMA cache_size = 65536');
184 0 0         if($self->{'logger'}) {
185 0           $self->{'logger'}->debug("read in $table from SQLite $slurp_file");
186             }
187 0           $self->{'type'} = 'DBI';
188             } else {
189 0           my $fin;
190 0           ($fin, $slurp_file) = File::pfopen::pfopen($dir, $table, 'csv.gz:db.gz');
191 0 0 0       if(defined($slurp_file) && (-r $slurp_file)) {
192 0           require Gzip::Faster;
193 0           Gzip::Faster->import();
194              
195 0           close($fin);
196 0           $fin = File::Temp->new(SUFFIX => '.csv', UNLINK => 0);
197 0           print $fin gunzip_file($slurp_file);
198 0           $slurp_file = $fin->filename();
199 0           $self->{'temp'} = $slurp_file;
200             } else {
201 0           ($fin, $slurp_file) = File::pfopen::pfopen($dir, $table, 'psv');
202 0 0         if(defined($fin)) {
203             # Pipe separated file
204 0           $args{'sep_char'} = '|';
205             } else {
206 0           ($fin, $slurp_file) = File::pfopen::pfopen($dir, $table, 'csv:db');
207             }
208             }
209 0 0 0       if(defined($slurp_file) && (-r $slurp_file)) {
210 0           close($fin);
211 0           my $sep_char = $args{'sep_char'};
212 0 0         if($args{'column_names'}) {
213             $dbh = DBI->connect("dbi:CSV:csv_sep_char=$sep_char", undef, undef,
214             {
215             csv_tables => {
216             $table => {
217 0           col_names => $args{'column_names'},
218             },
219             },
220             }
221             );
222             } else {
223 0           $dbh = DBI->connect("dbi:CSV:csv_sep_char=$sep_char");
224             }
225 0           $dbh->{'RaiseError'} = 1;
226              
227 0 0         if($self->{'logger'}) {
228 0           $self->{'logger'}->debug("read in $table from CSV $slurp_file");
229             }
230              
231 0           $dbh->{csv_tables}->{$table} = {
232             allow_loose_quotes => 1,
233             blank_is_undef => 1,
234             empty_is_undef => 1,
235             binary => 1,
236             f_file => $slurp_file,
237             escape_char => '\\',
238             sep_char => $sep_char,
239             # Don't do this, causes "Bizarre copy of HASH
240             # in scalar assignment in error_diag
241             # RT121127
242             # auto_diag => 1,
243             auto_diag => 0,
244             # Don't do this, it causes "Attempt to free unreferenced scalar"
245             # callbacks => {
246             # after_parse => sub {
247             # my ($csv, @rows) = @_;
248             # my @rc;
249             # foreach my $row(@rows) {
250             # if($row->[0] !~ /^#/) {
251             # push @rc, $row;
252             # }
253             # }
254             # return @rc;
255             # }
256             # }
257             };
258              
259             # my %options = (
260             # allow_loose_quotes => 1,
261             # blank_is_undef => 1,
262             # empty_is_undef => 1,
263             # binary => 1,
264             # f_file => $slurp_file,
265             # escape_char => '\\',
266             # sep_char => $sep_char,
267             # );
268              
269             # $dbh->{csv_tables}->{$table} = \%options;
270             # delete $options{f_file};
271              
272             # require Text::CSV::Slurp;
273             # Text::CSV::Slurp->import();
274             # $self->{'data'} = Text::CSV::Slurp->load(file => $slurp_file, %options);
275              
276 0           if(0) {
277             require Text::xSV::Slurp;
278             Text::xSV::Slurp->import();
279              
280             my @data = @{xsv_slurp(
281             shape => 'aoh',
282             text_csv => {
283             sep_char => $sep_char,
284             allow_loose_quotes => 1,
285             blank_is_undef => 1,
286             empty_is_undef => 1,
287             binary => 1,
288             escape_char => '\\',
289             },
290             # string => \join('', grep(!/^\s*(#|$)/, ))
291             file => $slurp_file
292             )};
293              
294             # Ignore blank lines or lines starting with # in the CSV file
295             unless($self->{no_entry}) {
296             @data = grep { $_->{'entry'} !~ /^\s*#/ } grep { defined($_->{'entry'}) } @data;
297             }
298             # $self->{'data'} = @data;
299             my $i = 0;
300             $self->{'data'} = ();
301             foreach my $d(@data) {
302             $self->{'data'}[$i++] = $d;
303             }
304             }
305 0           $self->{'type'} = 'CSV';
306             } else {
307 0           $slurp_file = File::Spec->catfile($dir, "$table.xml");
308 0 0         if(-r $slurp_file) {
309 0           $dbh = DBI->connect('dbi:XMLSimple(RaiseError=>1):');
310 0           $dbh->{'RaiseError'} = 1;
311 0 0         if($self->{'logger'}) {
312 0           $self->{'logger'}->debug("read in $table from XML $slurp_file");
313             }
314 0           $dbh->func($table, 'XML', $slurp_file, 'xmlsimple_import');
315             } else {
316 0           my @call_details = caller(0);
317 0           throw Error::Simple("Can't open $slurp_file called from " .
318             $call_details[2] . ' of ' . $call_details[1]);
319             }
320 0           $self->{'type'} = 'XML';
321             }
322             }
323              
324 0           $self->{$table} = $dbh;
325 0           my @statb = stat($slurp_file);
326 0           $self->{'_updated'} = $statb[9];
327              
328 0           return $self;
329             }
330              
331             # Returns a reference to an array of hash references of all the data meeting
332             # the given criteria
333             sub selectall_hashref {
334 0     0 0   my $self = shift;
335 0           my @rc = $self->selectall_hash(@_);
336 0           return \@rc;
337             }
338              
339             # Returns an array of hash references
340             sub selectall_hash {
341 0     0 0   my $self = shift;
342 0 0         my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0            
343              
344 0   0       my $table = $self->{table} || ref($self);
345 0           $table =~ s/.*:://;
346              
347 0 0 0       if((scalar(keys %params) == 0) && $self->{'data'}) {
348 0 0         if($self->{'logger'}) {
349 0           $self->{'logger'}->trace("$table: selectall_hash fast track return");
350             }
351             # This use of a temporary variable is to avoid
352             # "Implicit scalar context for array in return"
353             # return @{$self->{'data'}};
354 0           my @rc = @{$self->{'data'}};
  0            
355 0           return @rc;
356             }
357             # if((scalar(keys %params) == 1) && $self->{'data'} && defined($params{'entry'})) {
358             # }
359              
360 0 0         $self->_open() if(!$self->{$table});
361              
362 0           my $query;
363 0           my $done_where = 0;
364 0 0 0       if(($self->{'type'} eq 'CSV') && !$self->{no_entry}) {
365 0           $query = "SELECT * FROM $table WHERE entry IS NOT NULL AND entry NOT LIKE '#%'";
366 0           $done_where = 1;
367             } else {
368 0           $query = "SELECT * FROM $table";
369             }
370              
371 0           my @query_args;
372 0           foreach my $c1(sort keys(%params)) { # sort so that the key is always the same
373 0           my $arg = $params{$c1};
374 0 0         if(ref($arg)) {
375 0 0         if($self->{'logger'}) {
376 0           $self->{'logger'}->fatal("selectall_hash $query: argument is not a string");
377             }
378 0           throw Error::Simple("$query: argument is not a string: " . ref($arg));
379             }
380 0 0         if(!defined($arg)) {
381 0           my @call_details = caller(0);
382 0           throw Error::Simple("$query: value for $c1 is not defined in call from " .
383             $call_details[2] . ' of ' . $call_details[1]);
384             }
385 0 0         if($done_where) {
386 0 0         if($arg =~ /\@/) {
387 0           $query .= " AND $c1 LIKE ?";
388             } else {
389 0           $query .= " AND $c1 = ?";
390             }
391             } else {
392 0 0         if($arg =~ /\@/) {
393 0           $query .= " WHERE $c1 LIKE ?";
394             } else {
395 0           $query .= " WHERE $c1 = ?";
396             }
397 0           $done_where = 1;
398             }
399 0           push @query_args, $arg;
400             }
401 0 0         if(!$self->{no_entry}) {
402 0           $query .= ' ORDER BY entry';
403             }
404 0 0         if(!wantarray) {
405 0           $query .= ' LIMIT 1';
406             }
407 0 0         if($self->{'logger'}) {
408 0 0         if(defined($query_args[0])) {
409 0           $self->{'logger'}->debug("selectall_hash $query: ", join(', ', @query_args));
410             } else {
411 0           $self->{'logger'}->debug("selectall_hash $query");
412             }
413             }
414 0           my $key;
415             my $c;
416 0 0         if($c = $self->{cache}) {
    0          
417 0           $key = $query;
418 0 0         if(defined($query_args[0])) {
419 0           $key .= ' ' . join(', ', @query_args);
420             }
421 0 0         if(my $rc = $c->get($key)) {
422 0 0         if($self->{'logger'}) {
423 0           $self->{'logger'}->debug('cache HIT');
424             }
425             # This use of a temporary variable is to avoid
426             # "Implicit scalar context for array in return"
427             # return @{$rc};
428 0           my @rc = @{$rc};
  0            
429 0           return @rc;
430             }
431 0 0         if($self->{'logger'}) {
432 0           $self->{'logger'}->debug('cache MISS');
433             }
434             } elsif($self->{'logger'}) {
435 0           $self->{'logger'}->debug('cache not used');
436             }
437              
438 0 0         if(my $sth = $self->{$table}->prepare($query)) {
439 0 0         $sth->execute(@query_args) ||
440             throw Error::Simple("$query: @query_args");
441              
442 0           my @rc;
443 0           while(my $href = $sth->fetchrow_hashref()) {
444             # FIXME: Doesn't store in the cache
445 0 0         return $href if(!wantarray);
446 0           push @rc, $href;
447             }
448 0 0 0       if($c && wantarray) {
449 0           $c->set($key, \@rc, $self->{'cache_duration'});
450             }
451              
452 0           return @rc;
453             }
454 0 0         if($self->{'logger'}) {
455 0           $self->{'logger'}->warn("selectall_hash failure on $query: @query_args");
456             }
457 0           throw Error::Simple("$query: @query_args");
458             }
459              
460             # Returns a hash reference for one row in a table
461             # Special argument: table: determines the table to read from if not the default,
462             # which is worked out from the class name
463             sub fetchrow_hashref {
464 0     0 0   my $self = shift;
465 0 0         my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0            
466              
467 0   0       my $table = $self->{'table'} || ref($self);
468 0           $table =~ s/.*:://;
469              
470 0 0         $self->_open() if(!$self->{$table});
471              
472 0           my $query = 'SELECT * FROM ';
473 0 0         if(my $t = delete $params{'table'}) {
474 0           $query .= $t;
475             } else {
476 0           $query .= $table;
477             }
478 0           my $done_where = 0;
479 0 0 0       if(($self->{'type'} eq 'CSV') && !$self->{no_entry}) {
480 0           $query .= " WHERE entry IS NOT NULL AND entry NOT LIKE '#%'";
481 0           $done_where = 1;
482             }
483 0           my @query_args;
484 0           foreach my $c1(sort keys(%params)) { # sort so that the key is always the same
485 0 0         if(my $arg = $params{$c1}) {
486 0 0         if($done_where) {
487 0 0         if($arg =~ /\@/) {
488 0           $query .= " AND $c1 LIKE ?";
489             } else {
490 0           $query .= " AND $c1 = ?";
491             }
492             } else {
493 0 0         if($arg =~ /\@/) {
494 0           $query .= " WHERE $c1 LIKE ?";
495             } else {
496 0           $query .= " WHERE $c1 = ?";
497             }
498 0           $done_where = 1;
499             }
500 0           push @query_args, $arg;
501             }
502             }
503             # $query .= ' ORDER BY entry LIMIT 1';
504 0           $query .= ' LIMIT 1';
505 0 0         if($self->{'logger'}) {
506 0 0         if(defined($query_args[0])) {
507 0           my @call_details = caller(0);
508 0           $self->{'logger'}->debug("fetchrow_hashref $query: ", join(', ', @query_args),
509             ' called from ', $call_details[2], ' of ', $call_details[1]);
510             } else {
511 0           $self->{'logger'}->debug("fetchrow_hashref $query");
512             }
513             }
514 0           my $key;
515 0 0         if(defined($query_args[0])) {
516 0           $key = "fetchrow $query " . join(', ', @query_args);
517             } else {
518 0           $key = "fetchrow $query";
519             }
520 0           my $c;
521 0 0         if($c = $self->{cache}) {
522 0 0         if(my $rc = $c->get($key)) {
523 0           return $rc;
524             }
525             }
526 0 0         my $sth = $self->{$table}->prepare($query) or die $self->{$table}->errstr();
527 0 0         $sth->execute(@query_args) || throw Error::Simple("$query: @query_args");
528 0 0         if($c) {
529 0           my $rc = $sth->fetchrow_hashref();
530 0           $c->set($key, $rc, $self->{'cache_duration'});
531 0           return $rc;
532             }
533 0           return $sth->fetchrow_hashref();
534             }
535              
536             # Execute the given SQL on the data
537             # In an array context, returns an array of hash refs,
538             # in a scalar context returns a hash of the first row
539             sub execute {
540 0     0 0   my $self = shift;
541 0           my %args;
542              
543 0 0         if(ref($_[0]) eq 'HASH') {
    0          
    0          
544 0           %args = %{$_[0]};
  0            
545             } elsif(ref($_[0])) {
546 0           Carp::croak('Usage: execute(query => $query)');
547             } elsif(scalar(@_) % 2 == 0) {
548 0           %args = @_;
549             } else {
550 0           $args{'query'} = shift;
551             }
552              
553 0 0         Carp::croak('Usage: execute(query => $query)') unless(defined($args{'query'}));
554              
555 0   0       my $table = $self->{table} || ref($self);
556 0           $table =~ s/.*:://;
557              
558 0 0         $self->_open() if(!$self->{$table});
559              
560 0           my $query = $args{'query'};
561 0 0         if($self->{'logger'}) {
562 0           $self->{'logger'}->debug("execute $query");
563             }
564 0           my $sth = $self->{$table}->prepare($query);
565 0 0         $sth->execute() || throw Error::Simple($query);
566 0           my @rc;
567 0           while(my $href = $sth->fetchrow_hashref()) {
568 0 0         return $href if(!wantarray);
569 0           push @rc, $href;
570             }
571              
572 0           return @rc;
573             }
574              
575             # Time that the database was last updated
576             sub updated {
577 0     0 0   my $self = shift;
578              
579 0           return $self->{'_updated'};
580             }
581              
582             # Return the contents of an arbitrary column in the database which match the
583             # given criteria
584             # Returns an array of the matches, or just the first entry when called in
585             # scalar context
586              
587             # Set distinct to 1 if you're after a unique list
588             sub AUTOLOAD {
589 0     0     our $AUTOLOAD;
590 0           my $column = $AUTOLOAD;
591              
592 0           $column =~ s/.*:://;
593              
594 0 0         return if($column eq 'DESTROY');
595              
596 0 0         my $self = shift or return;
597              
598 0   0       my $table = $self->{table} || ref($self);
599 0           $table =~ s/.*:://;
600              
601 0 0         $self->_open() if(!$self->{$table});
602              
603 0 0         my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0            
604              
605 0           my $query;
606 0           my $done_where = 0;
607 0 0 0       if(wantarray && !delete($params{'distinct'})) {
608 0 0 0       if(($self->{'type'} eq 'CSV') && !$self->{no_entry}) {
609 0           $query = "SELECT $column FROM $table WHERE entry IS NOT NULL AND entry NOT LIKE '#%'";
610 0           $done_where = 1;
611             } else {
612 0           $query = "SELECT $column FROM $table";
613             }
614             } else {
615 0 0 0       if(($self->{'type'} eq 'CSV') && !$self->{no_entry}) {
616 0           $query = "SELECT DISTINCT $column FROM $table WHERE entry IS NOT NULL AND entry NOT LIKE '#%'";
617 0           $done_where = 1;
618             } else {
619 0           $query = "SELECT DISTINCT $column FROM $table";
620             }
621             }
622 0           my @args;
623 0           while(my ($key, $value) = each %params) {
624 0 0         if(defined($value)) {
625 0 0         if($done_where) {
626 0           $query .= " AND $key = ?";
627             } else {
628 0           $query .= " WHERE $key = ?";
629 0           $done_where = 1;
630             }
631 0           push @args, $value;
632             } else {
633 0 0         if($self->{'logger'}) {
634 0           $self->{'logger'}->debug("AUTOLOAD params $key isn't defined");
635             }
636 0 0         if($done_where) {
637 0           $query .= " AND $key IS NULL";
638             } else {
639 0           $query .= " WHERE $key IS NULL";
640 0           $done_where = 1;
641             }
642             }
643             }
644 0           $query .= " ORDER BY $column";
645 0 0         if(!wantarray) {
646 0           $query .= ' LIMIT 1';
647             }
648 0 0         if($self->{'logger'}) {
649 0 0 0       if(scalar(@args) && $args[0]) {
650 0           $self->{'logger'}->debug("AUTOLOAD $query: ", join(', ', @args));
651             } else {
652 0           $self->{'logger'}->debug("AUTOLOAD $query");
653             }
654             }
655 0   0       my $sth = $self->{$table}->prepare($query) || throw Error::Simple($query);
656 0 0         $sth->execute(@args) || throw Error::Simple($query);
657              
658 0 0         if(wantarray) {
659 0           return map { $_->[0] } @{$sth->fetchall_arrayref()};
  0            
  0            
660             }
661 0           return $sth->fetchrow_array(); # Return the first match only
662             }
663              
664             sub DESTROY {
665 0 0 0 0     if(defined($^V) && ($^V ge 'v5.14.0')) {
666 0 0         return if ${^GLOBAL_PHASE} eq 'DESTRUCT'; # >= 5.14.0 only
667             }
668 0           my $self = shift;
669              
670 0 0         if($self->{'temp'}) {
671 0           unlink delete $self->{'temp'};
672             }
673 0 0         if(my $table = delete $self->{'table'}) {
674 0           $table->finish();
675             }
676             }
677              
678             1;