File Coverage

lib/Geo/Coder/Free/DB.pm
Criterion Covered Total %
statement 137 194 70.6
branch 42 90 46.6
condition 12 33 36.3
subroutine 17 20 85.0
pod 0 7 0.0
total 208 344 60.4


line stmt bran cond sub pod time code
1             package Geo::Coder::Free::DB;
2              
3 6     6   57 use warnings;
  6         11  
  6         175  
4 6     6   28 use strict;
  6         9  
  6         103  
5              
6 6     6   27 use File::Glob;
  6         9  
  6         255  
7 6     6   63 use File::Basename;
  6         7  
  6         351  
8 6     6   7111 use DBI;
  6         76672  
  6         421  
9 6     6   51 use File::Spec;
  6         11  
  6         144  
10 6     6   1802 use File::pfopen 0.02;
  6         2498  
  6         233  
11 6     6   2630 use File::Temp;
  6         78965  
  6         368  
12 6     6   1330 use Gzip::Faster;
  6         5886  
  6         390  
13 6     6   2054 use DBD::SQLite::Constants qw/:file_open/; # For SQLITE_OPEN_READONLY
  6         44602  
  6         10736  
14              
15             our @databases;
16             our $directory;
17             our $logger;
18              
19             sub new {
20 6     6 0 1703 my $proto = shift;
21 6 50       30 my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0         0  
22              
23 6   33     47 my $class = ref($proto) || $proto;
24              
25             # init(\%args);
26              
27 6   66     89 return bless { logger => $args{'logger'} || $logger, directory => $args{'directory'} || $directory }, $class;
      33        
28             }
29              
30             # Can also be run as a class level Geo::Coder::Free::DB::init(directory => '../databases')
31             sub init {
32 5 50   5 0 228 my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0         0  
33              
34 5   33     33 $directory ||= $args{'directory'};
35 5   33     25 $logger ||= $args{'logger'};
36 5 50       14 if($args{'databases'}) {
37 0         0 @databases = $args{'databases'};
38             }
39 5 50       19 throw Error::Simple('directory not given') unless($directory);
40             }
41              
42             sub set_logger {
43 0     0 0 0 my $self = shift;
44              
45 0         0 my %args;
46              
47 0 0       0 if(ref($_[0]) eq 'HASH') {
    0          
48 0         0 %args = %{$_[0]};
  0         0  
49             } elsif(scalar(@_) % 2 == 0) {
50 0         0 %args = @_;
51             } else {
52 0         0 $args{'logger'} = shift;
53             }
54              
55 0         0 $self->{'logger'} = $args{'logger'};
56             }
57              
58             sub _open {
59 7     7   13 my $self = shift;
60             my %args = (
61             sep_char => '!',
62 7 50       44 ((ref($_[0]) eq 'HASH') ? %{$_[0]} : @_)
  0         0  
63             );
64              
65 7         19 my $table = ref($self);
66 7         46 $table =~ s/.*:://;
67              
68 7 100       27 if($self->{'logger'}) {
69 4         23 $self->{'logger'}->trace("_open $table");
70             }
71 7 100       42 return if($self->{$table});
72              
73             # Read in the database
74 6         8 my $dbh;
75              
76 6   33     22 my $directory = $self->{'directory'} || $directory;
77 6         98 my $slurp_file = File::Spec->catfile($directory, "$table.sql");
78              
79 6 100       229 if(-r $slurp_file) {
80 2         30 $dbh = DBI->connect("dbi:SQLite:dbname=$slurp_file", undef, undef, {
81             sqlite_open_flags => SQLITE_OPEN_READONLY,
82             });
83 2 100       3410 if($self->{'logger'}) {
84 1         6 $self->{'logger'}->debug("read in $table from SQLite $slurp_file");
85             }
86             } else {
87 4         9 my $fin;
88 4         21 ($fin, $slurp_file) = File::pfopen::pfopen($directory, $table, 'csv.gz:db.gz');
89 4 50 33     270 if(defined($slurp_file) && (-r $slurp_file)) {
90 0         0 $fin = File::Temp->new(SUFFIX => '.csv', UNLINK => 0);
91 0         0 print $fin gunzip_file($slurp_file);
92 0         0 $slurp_file = $fin->filename();
93 0         0 $self->{'temp'} = $slurp_file;
94             } else {
95 4         16 ($fin, $slurp_file) = File::pfopen::pfopen($directory, $table, 'csv:db');
96             }
97 4 50 33     406 if(defined($slurp_file) && (-r $slurp_file)) {
98 4         23 close($fin);
99 4         12 my $sep_char = $args{'sep_char'};
100 4 50       12 if($args{'column_names'}) {
101             $dbh = DBI->connect("dbi:CSV:csv_sep_char=$sep_char", undef, undef,
102             {
103             csv_tables => {
104             $table => {
105 4         55 col_names => $args{'column_names'},
106             }
107             }
108             }
109             );
110             } else {
111 0         0 $dbh = DBI->connect("dbi:CSV:csv_sep_char=$sep_char");
112             }
113 4         422185 $dbh->{'RaiseError'} = 1;
114              
115 4 100       54 if($self->{'logger'}) {
116 2         17 $self->{'logger'}->debug("read in $table from CSV $slurp_file");
117             }
118              
119 4         39 my %options = (
120             allow_loose_quotes => 1,
121             blank_is_undef => 1,
122             empty_is_undef => 1,
123             binary => 1,
124             f_file => $slurp_file,
125             escape_char => '\\',
126             sep_char => $sep_char,
127             );
128              
129 4         29 $dbh->{csv_tables}->{$table} = \%options;
130             # delete $options{f_file};
131              
132             # require Text::CSV::Slurp;
133             # Text::CSV::Slurp->import();
134             # $self->{'data'} = Text::CSV::Slurp->load(file => $slurp_file, %options);
135              
136 4         8596 if(0) {
137             require Text::xSV::Slurp;
138             Text::xSV::Slurp->import();
139              
140             my @data = @{xsv_slurp(
141             shape => 'aoh',
142             text_csv => {
143             sep_char => $sep_char,
144             allow_loose_quotes => 1,
145             blank_is_undef => 1,
146             empty_is_undef => 1,
147             binary => 1,
148             escape_char => '\\',
149             },
150             # string => \join('', grep(!/^\s*(#|$)/, ))
151             file => $slurp_file
152             )};
153              
154             # Don't use blank lines or comments
155             @data = grep { $_->{'entry'} !~ /^#/ } grep { defined($_->{'entry'}) } @data;
156             # $self->{'data'} = @data;
157             my $i = 0;
158             $self->{'data'} = ();
159             foreach my $d(@data) {
160             $self->{'data'}[$i++] = $d;
161             }
162             }
163             } else {
164 0         0 $slurp_file = File::Spec->catfile($directory, "$table.xml");
165 0 0       0 if(-r $slurp_file) {
166             # You'll need to install XML::Twig and
167             # AnyData::Format::XML
168             # The DBD::AnyData in CPAN doesn't work - grab a
169             # patched version from https://github.com/nigelhorne/DBD-AnyData.git
170 0         0 $dbh = DBI->connect('dbi:AnyData(RaiseError=>1):');
171 0         0 $dbh->{'RaiseError'} = 1;
172 0 0       0 if($self->{'logger'}) {
173 0         0 $self->{'logger'}->debug("read in $table from XML $slurp_file");
174             }
175 0         0 $dbh->func($table, 'XML', $slurp_file, 'ad_import');
176             } else {
177 0         0 throw Error::Simple("Can't open $directory/$table");
178             }
179             }
180             }
181              
182 6         26 push @databases, $table;
183              
184 6         14 $self->{$table} = $dbh;
185 6         103 my @statb = stat($slurp_file);
186 6         32 $self->{'_updated'} = $statb[9];
187             }
188              
189             # Returns a reference to an array of hash references of all the data meeting
190             # the given criteria
191             sub selectall_hashref {
192 1     1 0 3 my $self = shift;
193 1 50       5 my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0         0  
194              
195 1         2 my $table = ref($self);
196 1         6 $table =~ s/.*:://;
197              
198 1 50       10 $self->_open() if(!$self->{$table});
199              
200 1 0 33     5 if((scalar(keys %params) == 0) && $self->{'data'}) {
201 0 0       0 if($self->{'logger'}) {
202 0         0 $self->{'logger'}->trace("$table: selectall_hashref fast track return");
203             }
204 0         0 return $self->{'data'};
205             }
206              
207 1         3 my $query = "SELECT * FROM $table";
208 1         3 my @args;
209 1         3 foreach my $c1(keys(%params)) {
210 1 50       6 if(scalar(@args) == 0) {
211 1         3 $query .= ' WHERE';
212             } else {
213 0         0 $query .= ' AND';
214             }
215 1         3 $query .= " $c1 = ?";
216 1         3 push @args, $params{$c1};
217             }
218 1 50       5 if($self->{'logger'}) {
219 0         0 $self->{'logger'}->debug("selectall_hashref $query: " . join(', ', @args));
220             }
221 1         8 my $sth = $self->{$table}->prepare($query);
222 1 50       4159 $sth->execute(@args) || throw Error::Simple("$query: @args");
223 1         3171074 my @rc;
224 1         14 while (my $href = $sth->fetchrow_hashref()) {
225 2         146 push @rc, $href;
226             }
227              
228 1         50 return \@rc;
229             }
230              
231             # Returns a hash reference for one row in a table
232             sub fetchrow_hashref {
233 5     5 0 1412 my $self = shift;
234 5 100       25 my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  4         20  
235              
236 5         12 my $table = ref($self);
237 5         48 $table =~ s/.*:://;
238              
239 5 50       46 $self->_open() if(!$self->{table});
240              
241 5         19 my $query = "SELECT * FROM $table";
242 5         9 my @args;
243 5         19 foreach my $c1(keys(%params)) {
244 6 100       18 if(scalar(@args) == 0) {
245 5         15 $query .= ' WHERE';
246             } else {
247 1         2 $query .= ' AND';
248             }
249 6         16 $query .= " $c1 = ?";
250 6         16 push @args, $params{$c1};
251             }
252 5 100       21 if($self->{'logger'}) {
253 3         23 $self->{'logger'}->debug("fetchrow_hashref $query: " . join(', ', @args));
254             }
255 5         72 my $sth = $self->{$table}->prepare($query);
256 5 50       19704 $sth->execute(@args) || throw Error::Simple("$query: @args");
257 4         4536418 return $sth->fetchrow_hashref();
258             }
259              
260             # Execute the given SQL on the data
261             sub execute {
262 0     0 0 0 my $self = shift;
263 0 0       0 my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0         0  
264              
265 0         0 my $table = ref($self);
266 0         0 $table =~ s/.*:://;
267              
268 0 0       0 $self->_open() if(!$self->{table});
269              
270 0         0 my $query = $args{'query'};
271 0 0       0 if($self->{'logger'}) {
272 0         0 $self->{'logger'}->debug("fetchrow_hashref $query");
273             }
274 0         0 my $sth = $self->{$table}->prepare($query);
275 0 0       0 $sth->execute() || throw Error::Simple($query);
276 0         0 my @rc;
277 0         0 while (my $href = $sth->fetchrow_hashref()) {
278 0         0 push @rc, $href;
279             }
280              
281 0         0 return \@rc;
282             }
283              
284             # Time that the database was last updated
285             sub updated {
286 0     0 0 0 my $self = shift;
287              
288 0         0 return $self->{'_updated'};
289             }
290              
291             # Return the contents of an arbiratary column in the database which match the given criteria
292             # Returns an array of the matches, or just the first entry when called in scalar context
293             sub AUTOLOAD {
294 1     1   299 our $AUTOLOAD;
295 1         2 my $column = $AUTOLOAD;
296              
297 1         6 $column =~ s/.*:://;
298              
299 1 50       4 return if($column eq 'DESTROY');
300              
301 1 50       5 my $self = shift or return undef;
302              
303 1         2 my $table = ref($self);
304 1         4 $table =~ s/.*:://;
305              
306 1 50       6 $self->_open() if(!$self->{$table});
307              
308 1 50       6 my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0         0  
309              
310 1         4 my $query = "SELECT DISTINCT $column FROM $table";
311 1         1 my @args;
312 1         3 foreach my $c1(keys(%params)) {
313             # $query .= " AND $c1 LIKE ?";
314 2 100       5 if(scalar(@args) == 0) {
315 1         2 $query .= ' WHERE';
316             } else {
317 1         2 $query .= ' AND';
318             }
319 2         5 $query .= " $c1 = ?";
320 2         4 push @args, $params{$c1};
321             }
322 1         3 $query .= " ORDER BY $column";
323 1 50       4 if($self->{'logger'}) {
324 1 50       2 if(scalar(@args)) {
325 1         9 $self->{'logger'}->debug("AUTOLOAD $query: " . join(', ', @args));
326             } else {
327 0         0 $self->{'logger'}->debug("AUTOLOAD $query");
328             }
329             }
330 1   33     10 my $sth = $self->{$table}->prepare($query) || throw Error::Simple($query);
331 0 0       0 $sth->execute(@args) || throw Error::Simple($query);
332              
333 0 0       0 if(wantarray()) {
334 0         0 return map { $_->[0] } @{$sth->fetchall_arrayref()};
  0         0  
  0         0  
335             }
336 0         0 return $sth->fetchrow_array(); # Return the first match only
337             }
338              
339             sub DESTROY {
340 6 50 33 6   1948 if(defined($^V) && ($^V ge 'v5.14.0')) {
341 6 50       26 return if ${^GLOBAL_PHASE} eq 'DESTRUCT'; # >= 5.14.0 only
342             }
343 6         17 my $self = shift;
344              
345 6 50       255 if($self->{'temp'}) {
346 0           unlink $self->{'temp'};
347             }
348             }
349              
350             1;