File Coverage

blib/lib/Mojar/Mysql/Connector.pm
Criterion Covered Total %
statement 63 274 22.9
branch 20 126 15.8
condition 13 128 10.1
subroutine 13 50 26.0
pod 4 5 80.0
total 113 583 19.3


line stmt bran cond sub pod time code
1             package Mojar::Mysql::Connector;
2 3     3   46773 use DBI 1.4.3;
  3         46242  
  3         244  
3 3     3   34 use Mojo::Base 'DBI';
  3         8  
  3         32  
4              
5             # Register subclass structure
6             __PACKAGE__->init_rootclass;
7              
8             our $VERSION = 2.161;
9              
10 3     3   782 use Carp 'croak';
  3         8  
  3         190  
11 3     3   1805 use File::Spec::Functions 'catfile';
  3         2581  
  3         205  
12 3     3   1253 use Mojar::ClassShare 'have';
  3         1512  
  3         21  
13              
14             sub import {
15 3     3   38 my ($pkg, %param) = @_;
16 3         9 my $caller = caller;
17             # Helpers
18 3 100 50     30 $param{-connector} //= 1 if exists $param{-dbh} and $param{-dbh};
      66        
19 3 100 66     23 if (exists $param{-connector} and my $cname = delete $param{-connector}) {
20 1 50       5 $cname = 'connector' if "$cname" eq '1';
21 3     3   342 no strict 'refs';
  3         7  
  3         4312  
22 1         9 *{"${caller}::$cname"} = sub {
23 1     1   4199 my $self = shift;
24 1 50       5 if (@_) {
25 0 0       0 $self->{$cname} = (@_ > 1) ? Mojar::Mysql::Connector->new(@_) : shift;
26 0         0 return $self;
27             }
28 1   33     15 return $self->{$cname} //= Mojar::Mysql::Connector->new;
29 1         4 };
30 1 50 33     8 if (exists $param{-dbh} and my $hname = delete $param{-dbh}) {
31 1 50       5 $hname = 'dbh' if "$hname" eq '1';
32 1         5 *{"${caller}::$hname"} = sub {
33 0     0   0 my $self = shift;
34 0 0       0 if (@_) {
35 0 0       0 $self->{$hname} = (@_ > 1) ? $self->$cname->connect(@_) : shift;
36 0         0 return $self;
37             }
38             return $self->{$hname}
39 0 0 0     0 if defined $self->{$hname} and $self->{$hname}->ping;
40 0         0 return $self->{$hname} = $self->$cname->connect;
41 1         4 };
42             }
43             }
44             # Global defaults
45 3 50 66     13 if (%param and %{$pkg->Defaults}) {
  2         44  
46             # Already have defaults => check unchanged
47             # Not interested in defaults of Defaults => use hash not methods
48 0   0     0 my $ps = join ':', map +($_ .':'. ($param{$_} // 'undef')),
49             sort keys %param;
50             my $ds = join ':', map +($_ .':'. ($pkg->Defaults->{$_} // 'undef')),
51 0   0     0 sort keys %{$pkg->Defaults};
  0         0  
52 0 0       0 die "Redefining class defaults for $pkg" unless $ps eq $ds;
53             }
54 3 100       14 @{$pkg->Defaults}{keys %param} = values %param if %param;
  2         33  
55             # Debugging
56             $pkg->trace($param{TraceLevel})
57 3 50 33     3514 if exists $param{TraceLevel} and defined $param{TraceLevel};
58             }
59              
60             # Class attribute
61              
62             # Use a singleton object for holding use-time class defaults
63             have Defaults => sub { bless {} => ref $_[0] || $_[0] };
64              
65             # Attributes
66              
67             has quiesce_timeout => 500;
68              
69             my @DbdFields = qw(RaiseError PrintError PrintWarn AutoCommit TraceLevel
70             mysql_auto_reconnect mysql_enable_utf8);
71              
72             has RaiseError => 1;
73             has PrintError => 0;
74             has PrintWarn => 0;
75             has AutoCommit => 1;
76             has TraceLevel => 0;
77             has mysql_auto_reconnect => 0;
78             has mysql_enable_utf8 => 1;
79              
80             my @ConFields = qw(label cnfdir cnf cnfgroup);
81              
82             has 'label';
83             has cnfdir => '.';
84             has 'cnf';
85             has 'cnfgroup';
86              
87             my @DbiFields = qw(driver host port schema user password);
88              
89             has driver => 'mysql';
90             has 'host'; # eg 'localhost'
91             has 'port'; # eg 3306
92             has 'schema'; # eg 'test';
93             has 'user';
94             has 'password';
95              
96             # Public methods
97              
98             sub new {
99 5     5 1 15558 my ($proto, %param) = @_;
100             # $proto may contain defaults to be cloned
101             # %param may contain defaults for overriding
102 3         114 my %defaults = ref $proto ? ( %{ ref($proto)->Defaults }, %$proto )
103 5 100       20 : %{$proto->Defaults};
  2         60  
104 5 100       110 delete $defaults{$_} for grep { ref $proto and /^dbh\./ } keys %defaults;
  20         116  
105 5         33 return Mojo::Base::new($proto, %defaults, %param);
106             }
107              
108             sub connect {
109 0     0 1 0 my ($proto, @args) = @_;
110 0   0     0 my $class = ref $proto || $proto;
111 0 0 0     0 @args = $proto->dsn(@args) unless @args and $args[0] =~ /^DBI:/i;
112 0         0 my $dbh;
113 0         0 eval { $dbh = $class->SUPER::connect(@args) }
114 0 0       0 or do {
115 0         0 my $e = $@;
116 0         0 croak sprintf "Connection error\n%s\n%s", $proto->dsn_to_dump(@args), $e;
117             };
118 0         0 return $dbh;
119             }
120              
121             sub connection {
122 0   0 0 0 0 my ($self, $tag) = @_; $tag //= 'connection';
  0         0  
123 0 0 0     0 return $self->{"dbh.$tag"} if ($self->{"dbh.$tag"} //= $self->connect)->ping;
124 0         0 return $self->{"dbh.$tag"} = $self->connect;
125             }
126              
127             sub dsn {
128 2     2 1 13038 my ($proto, %param) = @_;
129 2         15 my $param = $proto->new(%param);
130              
131 2         35 my $cnf_txt = '';
132 2 50       12 if (my $cnf = $param->cnf) {
133             # MySQL .cnf file
134 0 0       0 $cnf .= '.cnf' unless $cnf =~ /\.cnf$/;
135 0 0 0     0 $cnf = catfile $param->cnfdir, $cnf if ! -r $cnf and defined $param->cnfdir;
136 0 0 0     0 croak "Failed to find/read .cnf file ($cnf)" unless -f $cnf and -r $cnf;
137              
138 0         0 $cnf_txt = ';mysql_read_default_file='. $cnf;
139 0 0       0 $cnf_txt .= ';mysql_read_default_group='. $param->cnfgroup
140             if defined $param->cnfgroup;
141             }
142              
143             # DBD params
144             # Only set private_config if it would have useful values
145 2         21 my %custom;
146 2   33     18 defined($param->$_) and $custom{$_} = $param->$_ for qw(label cnf cnfgroup);
147 2 50       51 my $dbd_param = %custom ? { private_config => {%custom} } : {};
148 2         21 $dbd_param->{$_} = $param->{$_} for grep /^mysql_/, keys %$param;
149 2         24 @$dbd_param{@DbdFields} = map $param->$_, @DbdFields;
150              
151             return (
152             'DBI:'. $param->driver .q{:}
153 2 50 33     102 . ($param->schema // $param->{db} // '')
    50 50        
154             . (defined $param->host ? q{;host=}. $param->host : '')
155             . (defined $param->port ? q{;port=}. $param->port : '')
156             . $cnf_txt,
157             $param->user,
158             $param->password,
159             $dbd_param
160             );
161             }
162              
163             sub dsn_to_dump {
164 0     0 1   my ($proto, @args) = @_;
165 0 0         @args = $proto->dsn unless @args;
166             # Occlude password
167 0 0 0       if ($args[2] and $_ = length $args[2] and $_ > 1) {
      0        
168 0           --$_;
169 0           my $blanks = '*' x $_;
170 0           $args[2] = substr($args[2], 0, 1). $blanks;
171             }
172 0           require Mojar::Util;
173 0           return Mojar::Util::dumper(@args);
174             }
175              
176             # ============
177             package Mojar::Mysql::Connector::db;
178             @Mojar::Mysql::Connector::db::ISA = 'DBI::db';
179              
180 3     3   24 use Carp 'croak';
  3         7  
  3         167  
181 3     3   1311 use Mojar::Util 'lc_keys';
  3         299371  
  3         188  
182 3     3   27 use Scalar::Util 'looks_like_number';
  3         7  
  3         6588  
183              
184             our $_as_hash = { Slice => {} };
185 0     0     sub as_hash { $_as_hash }
186              
187             # Public methods
188              
189 0     0     sub dsn { shift->get_info(2) }
190             # 2 : SQL_DATA_SOURCE_NAME
191              
192 0     0     sub mysqld_version { shift->get_info(18) }
193             # 18 : SQL_DBMS_VER
194              
195 0     0     sub identifier_quote { shift->get_info(29) }
196             # 29 : SQL_IDENTIFIER_QUOTE_CHAR
197              
198 0     0     sub identifier_separator { shift->get_info(41) }
199             # 41 : SQL_QUALIFIER_NAME_SEPARATOR
200              
201 0     0     sub async_mode { shift->get_info(10021) }
202             # 10021 : SQL_ASYNC_MODE
203              
204 0     0     sub async_max_statements { shift->get_info(10022) }
205             # 10022 : SQL_MAX_ASYNC_CONCURRENT_STATEMENTS
206              
207 0   0 0     sub thread_id { shift->{mysql_thread_id} // 0 }
208              
209             sub current_schema {
210 0     0     my ($self) = @_;
211 0           my ($schema) = $self->selectrow_array(
212             q{SELECT DATABASE()}
213             );
214 0           return $schema;
215             }
216              
217 0     0     sub session_var { shift->_var('SESSION', @_) }
218              
219             sub global_var {
220 0     0     my $self = shift;
221 0 0 0       return $self->_var(GLOBAL => @_)
      0        
222             if @_ >= 2 or @_ == 1 and $_[0] ne 'have_innodb';
223              
224 0           my $variables = $self->_var('GLOBAL');
225              
226             # Workaround for MySQL bug #59393 wrt ignore-builtin-innodb
227             $variables->{have_innodb} = 'NO'
228             if exists $variables->{ignore_builtin_innodb}
229 0 0 0       and ($variables->{ignore_builtin_innodb} // '') eq 'ON';
      0        
230              
231 0 0 0       return $variables->{have_innodb} if @_ == 1 and $_[0] eq 'have_innodb';
232 0           return $variables;
233             }
234              
235 0     0     sub disable_quotes { shift->session_var(sql_quote_show_create => 0) }
236              
237             sub enable_quotes {
238 0     0     my ($self, $value) = @_;
239 0   0       $value //= 1;
240 0           $self->session_var(sql_quote_show_create => $value)
241             }
242              
243 0     0     sub disable_fk_checks { shift->session_var(foreign_key_checks => 0) }
244              
245             sub enable_fk_checks {
246 0     0     my ($self, $value) = @_;
247 0   0       $value //= 1;
248 0           $self->session_var(foreign_key_checks => $value)
249             }
250              
251             sub schemata {
252 0     0     my ($self, @args) = @_;
253             # args[0] : schema pattern
254 0           my $schemata;
255             eval {
256 0           my $sql = q{SHOW DATABASES};
257 0 0         $sql .= sprintf q{ LIKE '%s'}, $args[0] if defined $args[0];
258 0 0         $schemata = $self->selectcol_arrayref($sql, $args[1]) or die;
259 0           @$schemata = grep !/^(?:\#|lost\+found)/, @$schemata;
260 0           1;
261             }
262 0 0         or do {
263 0   0       my $e = $@ // '';
264 0           croak "Failed to list schemata\n$e";
265             };
266 0           return $schemata;
267             }
268              
269             sub tables_and_views {
270 0     0     my ($self, @args) = @_;
271             # args[0] : schema
272             # args[1] : table pattern
273             # args[2] : type
274             # args[3] : attr
275 0   0       $args[2] //= 'TABLE,VIEW';
276 0           my $tables;
277             eval {
278 0           my $sth = $self->table_info('', @args);
279 0           @$tables = map $_->[2], @{$sth->fetchall_arrayref};
  0            
280 0           1;
281             }
282 0 0         or do {
283 0   0       my $e = $@ // '';
284 0           croak "Failed to list tables\n$e";
285             };
286 0           return $tables;
287             }
288              
289             sub real_tables {
290 0     0     my ($self, @args) = @_;
291             # args[0] : schema
292             # args[1] : table pattern
293             # args[2] : attr
294 0           return $self->tables_and_views(@args[0,1], 'TABLE', $args[2]);
295             }
296              
297             sub views {
298 0     0     my ($self, @args) = @_;
299             # args[0] : schema
300             # args[1] : table pattern
301             # args[2] : attr
302 0           return $self->tables_and_views(@args[0,1], 'VIEW', $args[2]);
303             }
304              
305             sub selectall_arrayref_hashrefs {
306 0     0     my ($self, $sql, $opts, @args) = @_;
307 0 0         if (defined $opts) {
308 0           $opts->{Slice} = {};
309             }
310             else {
311 0           $opts = $_as_hash;
312             }
313 0           return $self->selectall_arrayref($sql, $opts, @args);
314             }
315              
316             sub selectall_lookup {
317 0     0     my $self = shift;
318 0           my $rs = $self->selectall_arrayref(@_); # ($sql, $opts, @args)
319 0 0 0       return undef if @$rs >= 1 and @{$$rs[0]} != 2; # wrong qty cols
  0            
320 0           return {map @$_, @$rs}; # flatten
321             }
322              
323             sub threads {
324 0     0     my $p = shift->selectall_arrayref_hashrefs(q{SHOW FULL PROCESSLIST});
325 0           @$p = map lc_keys($_), @$p;
326 0           return $p;
327             }
328              
329             sub engines {
330 0     0     my ($self) = @_;
331              
332 0           my $engines = {};
333 0           my $e = $self->selectall_arrayref(q{SHOW ENGINES});
334 0           for (@$e) {
335 0 0         if ($_->[1] eq 'DEFAULT') {
336 0           $engines->{default} = lc $_->[0];
337 0           $engines->{lc $_->[0]} = 1;
338             }
339             else {
340 0 0         $engines->{lc $_->[0]} = $_->[1] eq 'YES' ? 1 : 0;
341             }
342             }
343 0           return $engines;
344             }
345              
346             sub statistics {
347 0     0     my ($self) = @_;
348              
349             # Arbitrary query to ensure results
350 0           ($_) = $self->selectrow_array(q{SELECT VERSION()});
351              
352 0           my $s = $self->selectall_arrayref(q{SHOW /*!50000 GLOBAL */ STATUS});
353 0           return lc_keys { map @$_, @$s };
354             }
355              
356             sub engine {
357 0     0     my ($self, $schema, $table) = @_;
358 0           my $engine;
359 0 0 0       if ($self->mysqld_version =~ /^(\d+)\./ and $1 >= 5) {
360 0           ($engine) = $self->selectrow_array(
361             q{SELECT ENGINE
362             FROM information_schema.TABLES
363             WHERE
364             TABLE_SCHEMA = ?
365             AND TABLE_NAME = ?},
366             undef,
367             $schema,
368             $table
369             );
370             }
371 0           return $engine;
372             }
373              
374             sub indices {
375 0     0     my ($self, $schema, $table) = @_;
376 0 0 0       croak 'Missing required schema name' unless defined $schema and length $schema;
377 0 0 0       croak 'Missing required table name' unless defined $table and length $table;
378 0           my $i = $self->selectall_arrayref(sprintf(
379             q{SHOW INDEXES FROM %s IN %s}, $table, $schema
380             ), $_as_hash
381             );
382             # $i is arrayref of hashrefs
383 0           lc_keys $_ for @$i;
384 0           return $i;
385             }
386              
387             sub table_status {
388 0     0     my ($self, $schema, $table_pattern) = @_;
389 0 0 0       croak 'Missing required schema name' unless defined $schema and length $schema;
390 0           my $sql = sprintf
391             q{SHOW TABLE STATUS FROM %s}, $schema;
392 0 0         $sql .= sprintf(q{ LIKE '%s'}, $table_pattern) if defined $table_pattern;
393 0           my $s = $self->selectall_arrayref($sql, $_as_hash);
394             # $s is arrayref of hashrefs
395 0           lc_keys $_ for @$s;
396 0           return $s;
397             }
398              
399             sub engine_status {
400 0     0     my ($self, $engine) = @_;
401 0   0       $engine //= 'InnoDB';
402 0 0         Carp::croak "Bad engine ($engine)" unless $engine =~ /^\w+$/;
403              
404 0           my ($major, $minor) = ($self->mysqld_version =~ /^(\d+)\.(\d+)\./);
405 0 0 0       my $i = $major > 5 ? 2 : $major == 5 && $minor >= 1 ? 2 : 0;
    0          
406 0   0       my $raw = ($self->selectrow_array(
407             qq{SHOW /*!50500 ENGINE */ $engine STATUS}))[$i]
408             // die "Failed to get engine ($engine) status\n";
409 0           $raw =~ s/\t/ /g;
410              
411 0           my ($title, $buffer, $status) = ('', '', {});
412 0           while ($raw =~ /^(.*)$/mg) {
413 0           my $line = $1;
414 0 0 0       if ($line =~ /^-+$/ and length $buffer) {
    0          
    0          
    0          
415             # Finish previous record
416 0           $status->{$title} = $buffer;
417 0           $title = $buffer = '';
418             }
419             elsif ($line =~ /^-+$/) {
420             # Start new record
421             }
422             elsif (not length $title) {
423 0           chomp $line;
424 0           $title = lc $line;
425 0           $title =~ s/\s+/_/g;
426 0           $title =~ s/\W//g;
427             }
428             elsif ($line =~ /^=+$/) {
429 0           next;
430             }
431             else {
432 0           $buffer .= $line . $/;
433             }
434             # Ignore final record
435             }
436 0           return $status;
437             }
438              
439             sub table_space {
440 0     0     my ($self, $schema, $table) = @_;
441 0           my $space;
442             eval {
443 0           ($space) = $self->selectrow_array(
444             q{SELECT CONCAT(TRUNCATE(DATA_FREE / 1024, 0), ' kB')
445             FROM information_schema.TABLES
446             WHERE
447             TABLE_SCHEMA = ?
448             AND TABLE_NAME = ?},
449             undef,
450             $schema, $table
451             );
452 0           $space ne '0 kB';
453             }
454 0 0         or eval {
455 0           my $comment = $self->table_status($schema, $table)->[0]{comment};
456 0 0         $space = $1 if $comment =~ /InnoDB free: (\d+ \w+)/;
457             };
458 0           return $space;
459             }
460              
461             sub date_from_today {
462 0     0     my ($self, $days, $format) = @_;
463 0   0       $days //= 0;
464 0   0       $format //= '%Y-%m-%d';
465 0           my ($date) = $self->selectrow_array(sprintf
466             q{SELECT DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL %s DAY), '%s')},
467             $days, $format
468             );
469 0           return $date;
470             }
471              
472             sub quiesce {
473 0     0     my ($self) = @_;
474              
475             # Record existing state
476 0   0       my $pct = $self->global_var('innodb_max_dirty_pages_pct')
477             // die "Failed to get dirty_pages info\n";
478 0   0       my $repl = $self->selectrow_hashref('SHOW SLAVE STATUS') // {};
479             Carp::croak "Cannot quiesce while replicating"
480 0 0 0       if ($repl->{Slave_SQL_Running} // '') =~ /^Yes/i;
481              
482             # Prepare callback to un-quiesce db
483             my $cb = sub {
484 0     0     eval { $self->do('UNLOCK TABLES') };
  0            
485 0   0       $self->global_var(innodb_max_dirty_pages_pct => shift // $pct);
486 0           };
487              
488             # Quiesce
489             eval {
490 0           $self->global_var(innodb_max_dirty_pages_pct => 0);
491 0           $self->do('FLUSH TABLES WITH READ LOCK');
492 0           my ($quiesced, $count) = (0, 0);
493 0           while (++$count < $self->quiesce_timeout) {
494 0           my $state = $self->engine_status('InnoDB')->{buffer_pool_and_memory};
495 0 0         if ($state =~ /^Modified db pages\s+(\d+)$/m) {
496 0 0         ++$quiesced, last if $1 == 0;
497             }
498             else {
499 0           die "Failed to check dirty pages\n";
500             }
501 0           sleep 1;
502             }
503 0           $quiesced;
504 0 0         } or do {
505 0   0       my $e = $@ // '';
506 0           eval { $cb->(); };
  0            
507 0           die "Failed to quiesce database: $e\n";
508             };
509 0           return $cb;
510             }
511              
512             # Private method
513              
514             sub _var {
515 0     0     my ($self, $scope) = (shift, shift);
516 0   0       $scope //= 'SESSION';
517              
518 0 0         unless (@_) {
519             # All vars
520 0           my $v = $self->selectall_arrayref(qq{SHOW $scope VARIABLES});
521 0           return { map @$_, @$v };
522             }
523              
524 0           my $var = shift;
525 0 0         unless (@_) {
526             # Getter
527 0           my ($value) = $self->selectrow_array(sprintf
528             q{SELECT @@%s.%s}, $scope, $var);
529 0           return $value;
530             }
531              
532             # Setter
533 0           my $value = shift;
534 0           my ($old, $new);
535             eval {
536 0           ($old) = $self->selectrow_array(sprintf
537             q{SELECT @@%s.%s}, $scope, $var);
538 0 0         $value = "'$value'" unless looks_like_number $value;
539 0           $self->do(qq{SET $scope $var = $value});
540 0           ($new) = $self->selectrow_array(sprintf
541             q{SELECT @@%s.%s}, $scope, $var);
542 0           1;
543             }
544 0 0         or do {
545 0   0       my $e = $@ // '';
546 0           croak "Failed to set var ($var)\n$e";
547             };
548 0 0         return wantarray ? ($old, $new) : $self;
549             }
550              
551             #TODO: clean up this ancient code
552             #sub insert_hash {
553             # my ($self, $schema, $table, $field_map) = @_;
554             # my @fields = keys %$field_map;
555             # my @values = values %$field_map;
556             # $self->do(sprintf(
557             #q{INSERT INTO %s.%s (%s) VALUES (%s)},
558             # $schema,
559             # $table,
560             # join(q{,}, @fields),
561             # join(q{,}, '?' x @fields)),
562             # undef,
563             # @values
564             # );
565             #}
566              
567             #TODO: clean up this ancient code
568             #sub search_hash {
569             # my ($self, $schema, $table, $field_map, @columns) = @_;
570             # my @fields = keys %$field_map;
571             # my @values = values %$field_map;
572             # my $wanted = scalar(@columns) ? join q{, }, @columns : q{*};
573             # my $where = '';
574             # $where = q{WHERE }. join q{ AND }, map '$_ = ?', @fields if @fields;
575             # $self->selectall_arrayref(sprintf(
576             #q{SELECT %s FROM %s.%s %s},
577             # $wanted, $schema, $table, $where),
578             # undef,
579             # @values
580             # );
581             #}
582              
583             # ============
584             package Mojar::Mysql::Connector::st;
585             @Mojar::Mysql::Connector::st::ISA = 'DBI::st';
586              
587             1;
588             __END__