File Coverage

lib/Geo/Coder/Free/DB.pm
Criterion Covered Total %
statement 142 192 73.9
branch 44 88 50.0
condition 13 33 39.3
subroutine 17 20 85.0
pod 0 7 0.0
total 216 340 63.5


line stmt bran cond sub pod time code
1             package Geo::Coder::Free::DB;
2              
3 6     6   56 use warnings;
  6         10  
  6         155  
4 6     6   25 use strict;
  6         10  
  6         94  
5              
6 6     6   23 use File::Glob;
  6         9  
  6         244  
7 6     6   29 use File::Basename;
  6         8  
  6         305  
8 6     6   6201 use DBI;
  6         75279  
  6         296  
9 6     6   51 use File::Spec;
  6         8  
  6         137  
10 6     6   1955 use File::pfopen 0.02;
  6         2439  
  6         251  
11 6     6   2690 use File::Temp;
  6         79079  
  6         391  
12 6     6   1316 use Gzip::Faster;
  6         5377  
  6         359  
13 6     6   1793 use DBD::SQLite::Constants qw/:file_open/; # For SQLITE_OPEN_READONLY
  6         44054  
  6         10905  
14              
15             our @databases;
16             our $directory;
17             our $logger;
18              
19             sub new {
20 6     6 0 1678 my $proto = shift;
21 6 50       30 my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0         0  
22              
23 6   33     31 my $class = ref($proto) || $proto;
24              
25             # init(\%args);
26              
27 6   66     75 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 232 my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0         0  
33              
34 5   33     36 $directory ||= $args{'directory'};
35 5   33     24 $logger ||= $args{'logger'};
36 5 50       16 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 37     37   63 my $self = shift;
60             my %args = (
61             sep_char => '!',
62 37 50       225 ((ref($_[0]) eq 'HASH') ? %{$_[0]} : @_)
  0         0  
63             );
64              
65 37         94 my $table = ref($self);
66 37         153 $table =~ s/.*:://;
67              
68 37 100       142 if($self->{'logger'}) {
69 5         29 $self->{'logger'}->trace("_open $table");
70             }
71 37 100       172 return if($self->{$table});
72              
73             # Read in the database
74 6         10 my $dbh;
75              
76 6   33     17 my $directory = $self->{'directory'} || $directory;
77 6         95 my $slurp_file = File::Spec->catfile($directory, "$table.sql");
78              
79 6 50       244 if(-r $slurp_file) {
80 0         0 $dbh = DBI->connect("dbi:SQLite:dbname=$slurp_file", undef, undef, {
81             sqlite_open_flags => SQLITE_OPEN_READONLY,
82             });
83 0 0       0 if($self->{'logger'}) {
84 0         0 $self->{'logger'}->debug("read in $table from SQLite $slurp_file");
85             }
86             } else {
87 6         12 my $fin;
88 6         32 ($fin, $slurp_file) = File::pfopen::pfopen($directory, $table, 'csv.gz:db.gz');
89 6 100 66     467 if(defined($slurp_file) && (-r $slurp_file)) {
90 2         18 $fin = File::Temp->new(SUFFIX => '.csv', UNLINK => 0);
91 2         1162 print $fin gunzip_file($slurp_file);
92 2         2390194 $slurp_file = $fin->filename();
93 2         59 $self->{'temp'} = $slurp_file;
94             } else {
95 4         12 ($fin, $slurp_file) = File::pfopen::pfopen($directory, $table, 'csv:db');
96             }
97 6 50 33     432 if(defined($slurp_file) && (-r $slurp_file)) {
98 6         89 close($fin);
99 6         23 my $sep_char = $args{'sep_char'};
100 6 100       20 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         43 col_names => $args{'column_names'},
106             }
107             }
108             }
109             );
110             } else {
111 2         37 $dbh = DBI->connect("dbi:CSV:csv_sep_char=$sep_char");
112             }
113 6         536052 $dbh->{'RaiseError'} = 1;
114              
115 6 100       75 if($self->{'logger'}) {
116 3         24 $self->{'logger'}->debug("read in $table from CSV $slurp_file");
117             }
118              
119 6         56 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 6         38 $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 6         10480 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         83 push @databases, $table;
183              
184 6         18 $self->{$table} = $dbh;
185 6         93 my @statb = stat($slurp_file);
186 6         57 $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 19     19 0 50 my $self = shift;
193 19 50       111 my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0         0  
194              
195 19         53 my $table = ref($self);
196 19         115 $table =~ s/.*:://;
197              
198 19 100       82 $self->_open() if(!$self->{$table});
199              
200 19 0 33     95 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 19         61 my $query = "SELECT * FROM $table";
208 19         38 my @args;
209 19         75 foreach my $c1(keys(%params)) {
210 19 50       67 if(scalar(@args) == 0) {
211 19         62 $query .= ' WHERE';
212             } else {
213 0         0 $query .= ' AND';
214             }
215 19         65 $query .= " $c1 = ?";
216 19         62 push @args, $params{$c1};
217             }
218 19 50       69 if($self->{'logger'}) {
219 0         0 $self->{'logger'}->debug("selectall_hashref $query: " . join(', ', @args));
220             }
221 19         135 my $sth = $self->{$table}->prepare($query);
222 19 50       39818 $sth->execute(@args) || throw Error::Simple("$query: @args");
223 19         47466039 my @rc;
224 19         202 while (my $href = $sth->fetchrow_hashref()) {
225 72         4488 push @rc, $href;
226             }
227              
228 19         1115 return \@rc;
229             }
230              
231             # Returns a hash reference for one row in a table
232             sub fetchrow_hashref {
233 35     35 0 2151 my $self = shift;
234 35 100       160 my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  22         127  
235              
236 35         101 my $table = ref($self);
237 35         174 $table =~ s/.*:://;
238              
239 35 50       269 $self->_open() if(!$self->{table});
240              
241 35         117 my $query = "SELECT * FROM $table";
242 35         61 my @args;
243 35         106 foreach my $c1(keys(%params)) {
244 60 100       136 if(scalar(@args) == 0) {
245 35         100 $query .= ' WHERE';
246             } else {
247 25         40 $query .= ' AND';
248             }
249 60         119 $query .= " $c1 = ?";
250 60         146 push @args, $params{$c1};
251             }
252 35 100       115 if($self->{'logger'}) {
253 4         24 $self->{'logger'}->debug("fetchrow_hashref $query: " . join(', ', @args));
254             }
255 35         304 my $sth = $self->{$table}->prepare($query);
256 35 50       97803 $sth->execute(@args) || throw Error::Simple("$query: @args");
257 35         6217778215 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   279 our $AUTOLOAD;
295 1         2 my $column = $AUTOLOAD;
296              
297 1         6 $column =~ s/.*:://;
298              
299 1 50       5 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       5 $self->_open() if(!$self->{$table});
307              
308 1 50       10 my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0         0  
309              
310 1         5 my $query = "SELECT DISTINCT $column FROM $table";
311 1         6 my @args;
312 1         4 foreach my $c1(keys(%params)) {
313             # $query .= " AND $c1 LIKE ?";
314 2 100       6 if(scalar(@args) == 0) {
315 1         3 $query .= ' WHERE';
316             } else {
317 1         2 $query .= ' AND';
318             }
319 2         6 $query .= " $c1 = ?";
320 2         5 push @args, $params{$c1};
321             }
322 1         3 $query .= " ORDER BY $column";
323 1 50       4 if($self->{'logger'}) {
324 1         7 $self->{'logger'}->debug("AUTOLOAD $query: " . join(', ', @args));
325             }
326 1   33     18 my $sth = $self->{$table}->prepare($query) || throw Error::Simple($query);
327 1 50       6697 $sth->execute(@args) || throw Error::Simple($query);
328              
329 1 50       361178337 if(wantarray()) {
330 0         0 return map { $_->[0] } @{$sth->fetchall_arrayref()};
  0         0  
  0         0  
331             }
332 1         31 return $sth->fetchrow_array(); # Return the first match only
333             }
334              
335             sub DESTROY {
336 6 50 33 6   3681 if(defined($^V) && ($^V ge 'v5.14.0')) {
337 6 50       27 return if ${^GLOBAL_PHASE} eq 'DESTRUCT'; # >= 5.14.0 only
338             }
339 6         17 my $self = shift;
340              
341 6 100       109 if($self->{'temp'}) {
342 2         45088 unlink $self->{'temp'};
343             }
344             }
345              
346             1;