File Coverage

blib/lib/Pepper/DB.pm
Criterion Covered Total %
statement 18 124 14.5
branch 0 26 0.0
condition 0 19 0.0
subroutine 6 19 31.5
pod 0 10 0.0
total 24 198 12.1


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