File Coverage

blib/lib/Pepper/DB.pm
Criterion Covered Total %
statement 15 128 11.7
branch 0 50 0.0
condition 0 21 0.0
subroutine 5 18 27.7
pod 0 10 0.0
total 20 227 8.8


line stmt bran cond sub pod time code
1             package Pepper::DB;
2              
3             $Pepper::VERSION = '1.3';
4              
5             # load needed third-part modules
6 1     1   2159 use DBI; # tim bounce, where would the world be without you? nowhere.
  1         18961  
  1         71  
7 1     1   661 use Try::Tiny;
  1         2113  
  1         58  
8              
9             # for checking the status of the DBI reference
10 1     1   8 use Scalar::Util qw(blessed);
  1         3  
  1         45  
11              
12             # time to grow up
13 1     1   6 use strict;
  1         2  
  1         18  
14 1     1   6 use warnings;
  1         1  
  1         1857  
15              
16             # create ourself and connect to the database
17             sub new {
18 0     0 0   my ($class,$args) = @_;
19             # $args should have:
20             # 'config' => the system configuration from Pepper::Utilities,
21             # 'utils' => an Pepper::Utilities object,
22              
23 0           my $config = $$args{config};
24              
25             # if $connect_to_database is empty, go with the main 'information_schema' system database
26 0   0       $$config{connect_to_database} ||= "information_schema";
27              
28             # cannot do a thing without the %$config - hard death
29 0 0 0       if (ref($config) ne 'HASH' || !$$config{database_username} || !$$config{database_password}) {
      0        
30 0           die "Cannot create DB object and continue without valid config hash.\n";
31             }
32            
33             # default DB server is localhost
34 0   0       $$config{database_server} ||= '127.0.0.1';
35            
36             # make the object
37             my $self = bless {
38             'config' => $config,
39             'database_server' => $$config{database_server},
40             'current_database' => $$config{connect_to_database},
41             'created' => time(),
42             'utils' => $$args{utils},
43 0           'connect_time' => 1,
44             }, $class;
45              
46             # now connect to the database and get a real DBI object into $self->{dbh}
47 0           $self->connect_to_database();
48              
49 0           return $self;
50             }
51              
52             # special method to connect or re-connect to the database
53             sub connect_to_database {
54 0     0 0   my $self = shift;
55            
56             # only do this if $self->{dbh} is not already a DBI object
57             return if $self->{dbh} && blessed($self->{dbh}) =~ /DBI/ &&
58 0 0 0       ( ( time()-$self->{connect_time} ) < 5 || $self->{dbh}->ping );
      0        
      0        
59              
60 0           my ($username, $password, $credentials, $dsn);
61              
62             # make the connection - fail and log if cannot connect
63            
64             # can support Mysql/MariaDB
65 0           $dsn = 'DBI:mysql:database='.$self->{current_database}.';host='.$self->{database_server}.';port=3306';
66             $self->{dbh} = DBI->connect($dsn, $self->{config}{database_username}, $self->{config}{database_password},{
67             PrintError => 0,
68             RaiseError => 1,
69             AutoCommit => 0,
70             mysql_enable_utf8 => 8
71 0 0         }) or $self->log_errors('Cannot connect to '.$self->{database_server}.': '.$DBI::errstr);
72              
73             # let's automatically reconnect if the connection is timed out
74 0           $self->{dbh}->{mysql_auto_reconnect} = 1;
75             # note that this doesn't seem to work too well
76              
77             # let's use UTC time in DB saves
78 0           $self->do_sql(qq{set time_zone = '+0:00'});
79              
80             # Set Long to 1000000 for long text...may need to adjust this
81 0           $self->{dbh}->{LongReadLen} = 1000000;
82              
83             # no pings for the first 5 seconds
84 0           $self->{connect_time} = time();
85            
86             # $self->{dbh} is now ready to go
87             }
88              
89             # method to change the current working database for a connection
90             sub change_database {
91             # required argument is the database they want to switch into
92 0     0 0   my ($self,$database_name) = @_;
93              
94             # nothing to do if that's not specified
95 0 0         return if !$database_name;
96              
97             # no funny business
98 0 0         return 'Bad Name' if $database_name =~ /[^a-z0-9\_]/i;
99              
100             # put in object attribute
101 0           $self->{current_database} = $database_name;
102              
103             # make sure we are connected to the DB
104 0           $self->connect_to_database();
105              
106             # pretty easy
107 0           $self->{dbh}->do(qq{use $database_name});
108              
109             }
110              
111              
112             # comma_list_select: same as list_select, but returns a commafied list rather than an array
113             sub comma_list_select {
114             # grab args
115 0     0 0   my ($self,$sql,$bind_values) = @_;
116              
117             # rely on our brother upstairs
118 0           my $results = $self->list_select($sql,$bind_values);
119              
120             # nothing found? just return
121 0 0         if (!$$results[0]) {
122 0           return;
123             } else { # otherwise, return our comma-separated version of this
124 0           return join(',',@$results);
125             }
126             }
127              
128             # utility method to commit changes; I know DBI does it. This is how I want to do it.
129             sub commit {
130 0     0 0   my $self = shift;
131 0           $self->do_sql('commit');
132             }
133              
134             # do_sql: our most flexible way to execute SQL statements
135             sub do_sql {
136             # grab args
137 0     0 0   my ($self,$sql,$bind_values) = @_;
138              
139             # declare vars
140 0           my ($results, $sth, $cleared_deadlocks);
141              
142             # sql statement to execute and if placeholders used, arrayref of values
143              
144             # make sure we are connected to the DB
145 0           $self->connect_to_database();
146              
147             # i shouldn't need this, but just in case
148 0 0         if (!$self->{dbh}) {
149 0           $self->log_errors(qq{Missing DB Connection for $sql.});
150             }
151              
152             # prepare the SQL
153 0 0         $sth = $self->{dbh}->prepare($sql) or $self->log_errors(qq{Error preparing $sql: }.$self->{dbh}->errstr());
154            
155             # ready to execute, but we want to plan for some possible deadlocks, since InnoDB is still not perfect
156 0           $cleared_deadlocks = 0;
157 0           while ($cleared_deadlocks == 0) {
158 0           $cleared_deadlocks = 1; # if it succeeds once, we can stop
159             # attempt to execute; catch any errors and keep retrying in the event of a deadlock
160             try {
161             # use $@values if has placeholders
162 0 0   0     if ($bind_values) {
163 0           $sth->execute(@$bind_values);
164             } else { # plain-jane
165 0           $sth->execute;
166             }
167             }
168             # catch the errors
169             catch {
170 0 0   0     if ($_ =~ /Deadlock/) { # retry after three seconds
171 0           sleep(3);
172 0           $cleared_deadlocks = 0;
173             } else { # regular error: rollback, log error, and die
174 0           $self->{dbh}->rollback;
175 0           $$bind_values[0] = 'No values';
176 0           $self->log_errors(qq{Error executing $sql (@$bind_values): }.$_);
177 0           $cleared_deadlocks = 1;
178             }
179             }
180 0           }
181              
182             # i like pretty formatting/spacing for my code, maybe too much
183 0           $sql =~ s/^\s+//g;
184              
185             # if SELECT, grab all the results into a arrayref of arrayrefs
186 0 0         if ($sql =~ /^select|^show|^desc/i) {
    0          
187             # snatch it
188 0           $results = $sth->fetchall_arrayref;
189             # here's how you use this:
190             # while (($one,$two) = @{shift(@$results)}) { ... }
191              
192             # clean up
193 0           $sth->finish;
194              
195             # send out results
196 0           return $results;
197              
198             # if it is an insert, let's stash the last-insert ID, mainly for BaseModel's save()
199             } elsif ($sql =~ /^(insert|replace)/i) {
200 0           $self->{last_insert_id} = $sth->{'mysql_insertid'};
201             }
202              
203             # any finally, clean (will only still be here for insert, replace, or update statements)
204 0           $sth->finish;
205             }
206              
207              
208             # list_select: easily execute sql SELECTs that will return a simple array; returns an arrayref
209             sub list_select {
210             # grab args
211 0     0 0   my ($self,$sql,$bind_values) = @_;
212             # sql statement to execute and if placeholders used, arrayref of values
213              
214             # declare vars
215 0           my ($sth, @data, @sendBack);
216              
217             # make sure we are connected to the DB
218 0           $self->connect_to_database();
219              
220             # we should never have this error condition, but just in case
221 0 0         if (!$self->{dbh}) {
222 0           $self->log_errors(qq{Missing DB Connection for $sql.});
223             }
224              
225             # prep & execute the sql
226 0           $sth = $self->{dbh}->prepare($sql);
227             # use $@values if has placeholders
228 0 0         if ($bind_values) {
229 0 0         $sth->execute(@$bind_values) or $self->log_errors(qq{Error executing $sql: }.$self->{dbh}->errstr);
230              
231             } else { # place-jane
232 0 0         $sth->execute or $self->log_errors(qq{Error executing $sql: }.$self->{dbh}->errstr);
233             }
234            
235             # grab the data & toss it into an array
236 0           while ((@data)=$sth->fetchrow_array) {
237 0           push(@sendBack,$data[0]); # take left most one, so it's 100%, for-sure one-dimensional (no funny business)
238             }
239              
240             # send back the arrayref
241 0           return \@sendBack;
242             }
243              
244             # subroutine to use the Pepper::Utilities's logging and return functions to capture errors and return a proper message
245             sub log_errors {
246 0     0 0   my ($self,$error_message) = @_;
247              
248             # default message in cause of blank
249 0   0       $error_message ||= 'Database error.';
250              
251             # log and then send the message
252 0           $self->{utils}->logger($error_message,'database_errors');
253 0           $self->{utils}->send_response($error_message,1);
254             }
255              
256             # quick_select: easily execute sql SELECTs that will return one row; returns live array
257             sub quick_select {
258             # grab args
259 0     0 0   my ($self,$sql,$bind_values) = @_;
260             # sql statement to execute and if placeholders used, arrayref of values
261              
262             # declare vars
263 0           my (@data, $sth);
264              
265             # make sure we are connected to the DB
266 0           $self->connect_to_database();
267              
268             # we should never have this error condition, but just in case
269 0 0         if (!$self->{dbh}) {
270 0           $self->log_errors(qq{Missing DB Connection for $sql.});
271             }
272              
273             # prep & execute the sql
274 0           $sth = $self->{dbh}->prepare($sql);
275              
276             # use $@values if has placeholders
277 0 0         if ($$bind_values[0]) {
278 0 0         $sth->execute(@$bind_values) or die $sth->errstr; # or $self->log_errors(qq{Error executing $sql (@$bind_values): }.$self->{dbh}->errstr);
279             } else { # plain-jane
280 0 0         $sth->execute or die $sth->errstr; # or $self->log_errors(qq{Error executing $sql: }.$self->{dbh}->errstr);
281             }
282              
283             # grab the data
284 0           (@data) = $sth->fetchrow_array;
285              
286             # return a real, live array, not a memory reference for once...just easier this way,
287             # since much of the time, you are just sending a single piece of data
288 0           return (@data);
289             }
290              
291             # sql_hash: take an sql command and return a hash of results; my absolute personal favorite
292             sub sql_hash {
293             # grab args: the sql_statement text string (required), then an arrayref for bind-variables (highly-recommended)
294             # and then an arrayref of alternative sub-key names for your second-level hashes
295 0     0 0   my ($self, $sql, $bind_values, $names) = @_;
296             # the command to run and optional: list of names to key the data by...if blank, i'll use @cols from the sql
297              
298             # declare vars
299 0           my ($c, $cnum, $columns, $key, $kill, $num, $rest, $sth, %our_hash, @cols, @data, @keys);
300              
301 0 0         if (!$$names[0]) { # determine the column names and make them an array
302 0           ($columns,$rest) = split /\sfrom\s/i, $sql;
303 0           $columns =~ s/count\(\*\)\ as\ //; # allow for 'count(*) as NAME' columns
304 0           $columns =~ s/select|[^0-9a-z\_\,]//gi; # take out "SELECT" and spaces
305 0           $columns =~ s/\,\_\,/_/; # account for a lot of this: concat(code,'_',server_id)
306              
307 0           (@$names) = split /\,/, $columns;
308 0           $kill = shift (@$names); # kill the first one, as that one will be our key
309             }
310              
311             # make sure we are connected to the DB
312 0           $self->connect_to_database();
313              
314             # this is easy: run the command, and build a hash keyed by the first column, with the column names as sub-keys
315             # note that this works best if there are at least two columns listed
316 0           $num = 0;
317 0 0         if (!$self->{dbh}) {
318 0           $self->log_errors(qq{Missing DB Connection for $sql.});
319             }
320            
321             # prep & execute the sql
322 0           $sth = $self->{dbh}->prepare($sql);
323              
324             # placeholders?
325 0 0         if ($$bind_values[0]) {
326 0 0         $sth->execute(@$bind_values) or $self->log_errors(qq{Error executing $sql: }.$self->{dbh}->errstr);
327             } else {
328 0 0         $sth->execute or $self->log_errors(qq{Error executing $sql: }.$self->{dbh}->errstr);
329             }
330              
331             # this does not seem any faster, oddly:
332             # my ($results_arrays, $result_array);
333             #$results_arrays = $self->{dbh}->selectall_arrayref($sql, {}, @$bind_values)
334             # or $self->log_errors(qq{Error executing $sql: }.$self->{dbh}->errstr);
335              
336             # foreach $result_array (@$results_arrays) {
337 0           while(($key,@data)=$sth->fetchrow_array) {
338             # $key = shift @$result_array;
339 0           $cnum = 0;
340 0           foreach $c (@$names) {
341 0           $our_hash{$key}{$c} = $data[$cnum]; # shift @$result_array; #
342 0           $cnum++;
343             }
344 0           $keys[$num] = $key;
345 0           $num++;
346             }
347              
348             # return a reference to the hash along with the ordered set of keys
349 0           return (\%our_hash, \@keys);
350             }
351              
352             # empty destroy for now
353             sub DESTROY {
354 0     0     my $self = shift;
355            
356             # have to do this since we have autocommit off
357 0           $self->do_sql('rollback');
358 0           my $rc = $self->{dbh}->disconnect;
359            
360             }
361              
362             # all done
363             1;
364              
365             __END__
366              
367             =head1 NAME
368              
369             Pepper::DB
370              
371             =head1 DESCRIPTION
372              
373             Provides database methods for Pepper, including support for MySQL/MariaDB.
374             This object is created as part of a new Pepper object, so all methods are documented
375             in that package's POD. Please see 'perldoc Pepper' for more details.