1package DBIx::QuickDB::Driver::MySQL;
2use strict;
3use warnings;
4
5our $VERSION = '0.000021';
6
7use IPC::Cmd qw/can_run/;
8use DBIx::QuickDB::Util qw/strip_hash_defaults/;
9use Scalar::Util qw/reftype/;
10use Carp qw/confess/;
11
12use parent 'DBIx::QuickDB::Driver';
13
14use DBIx::QuickDB::Util::HashBase qw{
15    -data_dir -temp_dir -socket -pid_file -cfg_file
16
17    -mysqld -mysql
18
19    -dbd_driver
20    -mysqld_provider
21    -use_bootstrap
22    -use_installdb
23
24    -character_set_server
25
26    -config
27};
28
29my ($MYSQLD, $MYSQL, $DBDMYSQL, $DBDMARIA, $INSTALLDB);
30
31BEGIN {
32    local $@;
33
34    $MYSQLD = can_run('mysqld');
35    $MYSQL  = can_run('mysql');
36    $INSTALLDB = can_run('mysql_install_db');
37
38    $DBDMYSQL = eval { require DBD::mysql;   'DBD::mysql' };
39    $DBDMARIA = eval { require DBD::MariaDB; 'DBD::MariaDB' };
40}
41
42sub version_string {
43    my $binary;
44
45    # Go in reverse order assuming the last param hash provided is most important
46    for my $arg (reverse @_) {
47        my $type = reftype($arg) or next;    # skip if not a ref
48        next unless $type eq 'HASH';         # We have a hashref, possibly blessed
49
50        # If we find a launcher we are done looping, we want to use this binary.
51        $binary = $arg->{+MYSQLD} and last;
52    }
53
54    # If no args provided one to use we fallback to the default from $PATH
55    $binary ||= $MYSQLD;
56
57    # Call the binary with '-V', capturing and returning the output using backticks.
58    return `$binary -V`;
59}
60
61sub list_env_vars {
62    my $self = shift;
63    return (
64        $self->SUPER::list_env_vars(),
65        qw{
66            LIBMYSQL_ENABLE_CLEARTEXT_PLUGIN LIBMYSQL_PLUGINS
67            LIBMYSQL_PLUGIN_DIR MYSQLX_TCP_PORT MYSQLX_UNIX_PORT MYSQL_DEBUG
68            MYSQL_GROUP_SUFFIX MYSQL_HISTFILE MYSQL_HISTIGNORE MYSQL_HOME
69            MYSQL_HOST MYSQL_OPENSSL_UDF_DH_BITS_THRESHOLD
70            MYSQL_OPENSSL_UDF_DSA_BITS_THRESHOLD
71            MYSQL_OPENSSL_UDF_RSA_BITS_THRESHOLD MYSQL_PS1 MYSQL_PWD
72            MYSQL_SERVER_PREPARE MYSQL_TCP_PORT MYSQL_TEST_LOGIN_FILE
73            MYSQL_TEST_TRACE_CRASH MYSQL_TEST_TRACE_DEBUG MYSQL_UNIX_PORT
74        }
75    );
76}
77
78sub _default_paths {
79    return (
80        mysqld => $MYSQLD,
81        mysql  => $MYSQL,
82    );
83}
84
85sub _default_config {
86    my $self = shift;
87
88    my $dir = $self->dir;
89    my $data_dir = $self->data_dir;
90    my $temp_dir = $self->temp_dir;
91    my $pid_file = $self->pid_file;
92    my $socket   = $self->socket;
93
94    my $provider = $self->{+MYSQLD_PROVIDER};
95
96    return (
97        client => {
98            'socket' => $socket,
99        },
100
101        mysql_safe => {
102            'socket' => $socket,
103        },
104
105        mysqld => {
106            'datadir'  => $data_dir,
107            'pid-file' => $pid_file,
108            'socket'   => $socket,
109            'tmpdir'   => $temp_dir,
110
111            'secure_file_priv'               => $dir,
112            'default_storage_engine'         => 'InnoDB',
113            'innodb_buffer_pool_size'        => '20M',
114            'key_buffer_size'                => '20M',
115            'max_connections'                => '100',
116            'server-id'                      => '1',
117            'skip_grant_tables'              => '1',
118            'skip_external_locking'          => '',
119            'skip_networking'                => '1',
120            'skip_name_resolve'              => '1',
121            'max_allowed_packet'             => '1M',
122            'max_binlog_size'                => '20M',
123            'myisam_sort_buffer_size'        => '8M',
124            'net_buffer_length'              => '8K',
125            'read_buffer_size'               => '256K',
126            'read_rnd_buffer_size'           => '512K',
127            'sort_buffer_size'               => '512K',
128            'table_open_cache'               => '64',
129            'thread_cache_size'              => '8',
130            'thread_stack'                   => '192K',
131            'innodb_io_capacity'             => '2000',
132            'innodb_max_dirty_pages_pct'     => '0',
133            'innodb_max_dirty_pages_pct_lwm' => '0',
134
135            $provider eq 'percona'
136            ? (
137                'character_set_server' => $self->{+CHARACTER_SET_SERVER},
138              )
139            : (
140                'character_set_server' => $self->{+CHARACTER_SET_SERVER},
141                'query_cache_limit'    => '1M',
142                'query_cache_size'     => '20M',
143            ),
144        },
145
146        mysql => {
147            'socket'         => $socket,
148            'no-auto-rehash' => '',
149        },
150    );
151}
152
153sub viable {
154    my $this = shift;
155    my ($spec) = @_;
156
157    my %check = (ref($this) ? %$this : (), $this->_default_paths, %$spec);
158
159    my @bad;
160
161    push @bad => "Could not load either 'DBD::mysql' or 'DBD::MariaDB', needed for everything"
162        unless $DBDMYSQL || $DBDMARIA;
163
164    if ($spec->{bootstrap}) {
165        push @bad => "'mysqld' command is missing, needed for bootstrap" unless $check{mysqld} && -x $check{mysqld};
166    }
167    elsif ($spec->{autostart}) {
168        push @bad => "'mysqld' command is missing, needed for autostart" unless $check{mysqld} && -x $check{mysqld};
169    }
170
171    if ($spec->{load_sql}) {
172        push @bad => "'mysql' command is missing, needed for load_sql" unless $check{mysql} && -x $check{mysql};
173    }
174
175    if ($check{+MYSQLD} || $MYSQLD) {
176        my $version = $this->version_string;
177        if ($version && $version =~ m/(\d+)\.(\d+)\.(\d+)/) {
178            my ($a, $b, $c) = ($1, $2, $3);
179            push @bad => "'mysqld' is too old ($a.$b.$c), need at least 5.6.0"
180                if $a < 5 || ($a == 5 && $b < 6);
181        }
182    }
183
184    return (1, undef) unless @bad;
185    return (0, join "\n" => @bad);
186}
187
188sub init {
189    my $self = shift;
190    $self->SUPER::init();
191
192    # Percona is the more restrictive, so fallback to mariadb behavior for
193    # now. Add patches for more variants if needed.
194    unless ($self->{+MYSQLD_PROVIDER}) {
195        if ($self->version_string =~ m/(mariadb|percona)/i) {
196            $self->{+MYSQLD_PROVIDER} = lc($1);
197
198            if ($self->{+MYSQLD_PROVIDER} eq 'percona') {
199                my $binary = $self->{+MYSQLD} || $MYSQLD;
200                my $help = `$binary --help --verbose 2>&1`;
201
202                if ($help =~ m/--bootstrap/) {
203                    $self->{+USE_BOOTSTRAP} = 1;
204
205                    $self->{+USE_INSTALLDB} = $INSTALLDB ? 1 : 0;
206                }
207            }
208        }
209        else {
210            my $binary = $self->{+MYSQLD} || $MYSQLD;
211            my $help = `$binary --help --verbose 2>&1`;
212
213            if ($help =~ m/(mariadb|percona)/i) {
214                $self->{+MYSQLD_PROVIDER} = lc($1);
215            }
216            elsif ($help =~ m/--bootstrap/) {
217                $self->{+MYSQLD_PROVIDER} = 'mariadb';
218            }
219            elsif ($help =~ m/--initialize/) {
220                $self->{+MYSQLD_PROVIDER} = 'percona';
221            }
222        }
223    }
224
225    confess "Could not determine mysqld provider (" . ($self->{+MYSQLD} || $MYSQLD)  . ") please specify mysqld_prover => mariadb|percona"
226        unless $self->{+MYSQLD_PROVIDER};
227
228    $self->{+DBD_DRIVER} //= $DBDMARIA || $DBDMYSQL;
229
230    $self->{+CHARACTER_SET_SERVER} //= 'UTF8MB4';
231
232    $self->{+DATA_DIR} = $self->{+DIR} . '/data';
233    $self->{+TEMP_DIR} = $self->{+DIR} . '/temp';
234    $self->{+PID_FILE} = $self->{+DIR} . '/mysql.pid';
235    $self->{+CFG_FILE} = $self->{+DIR} . '/my.cfg';
236
237    $self->{+SOCKET} ||= $self->{+DIR} . '/mysql.sock';
238
239    $self->{+USERNAME} ||= 'root';
240
241    my %defaults = $self->_default_paths;
242    $self->{$_} ||= $defaults{$_} for keys %defaults;
243
244    my %cfg_defs = $self->_default_config;
245    my $cfg = $self->{+CONFIG} ||= {};
246
247    for my $key (keys %cfg_defs) {
248        if (defined $cfg->{$key}) {
249            my $subdft = $cfg_defs{$key};
250            my $subcfg = $cfg->{$key};
251
252            for my $skey (%$subdft) {
253                next if defined $subcfg->{$skey};
254                $subcfg->{$skey} = $subdft->{$skey};
255            }
256        }
257        else {
258            $cfg->{$key} = $cfg_defs{$key};
259        }
260    }
261}
262
263sub clone_data {
264    my $self = shift;
265
266    my $config = strip_hash_defaults(
267        $self->{+CONFIG},
268        { $self->_default_config },
269    );
270
271    return (
272        $self->SUPER::clone_data(),
273
274        CONFIG()          => $config,
275        MYSQLD()          => $self->{+MYSQLD},
276        MYSQL()           => $self->{+MYSQL},
277        DBD_DRIVER()      => $self->{+DBD_DRIVER},
278        MYSQLD_PROVIDER() => $self->{+MYSQLD_PROVIDER},
279    );
280}
281
282sub write_config {
283    my $self = shift;
284    my (%params) = @_;
285
286    my $cfg_file = $self->{+CFG_FILE};
287    open(my $cfh, '>', $cfg_file) or die "Could not open config file: $!";
288    my $conf = $self->{+CONFIG};
289    for my $section (sort keys %$conf) {
290        my $sconf = $conf->{$section} or next;
291
292        $sconf = { %$sconf, %{$params{add}} } if $params{add};
293
294        print $cfh "[$section]\n";
295        for my $key (sort keys %$sconf) {
296            my $val = $sconf->{$key};
297            next unless defined $val;
298
299            next if $params{skip} && ($key =~ $params{skip} || $val =~ $params{skip});
300
301            if (length($val)) {
302                print $cfh "$key = $val\n";
303            }
304            else {
305                print $cfh "$key\n";
306            }
307        }
308
309        print $cfh "\n";
310    }
311    close($cfh);
312
313    return;
314}
315
316sub bootstrap {
317    my $self = shift;
318
319    my $data_dir = $self->{+DATA_DIR};
320    my $temp_dir = $self->{+TEMP_DIR};
321
322    mkdir($data_dir) or die "Could not create data dir: $!";
323    mkdir($temp_dir) or die "Could not create temp dir: $!";
324
325
326    my $init_file = "$self->{+DIR}/init.sql";
327    open(my $init, '>', $init_file) or die "Could not open init file: $!";
328    print $init "CREATE DATABASE quickdb;\n";
329    close($init);
330
331    my $provider = $self->{+MYSQLD_PROVIDER};
332
333    if ($provider eq 'percona') {
334
335        if ($self->{+USE_BOOTSTRAP}) {
336            if($self->{+USE_INSTALLDB}) {
337                local $ENV{PERL5LIB} = "";
338                $self->run_command([$INSTALLDB, '--datadir=' . $data_dir]);
339            }
340            $self->write_config();
341            $self->run_command([$self->start_command, '--bootstrap'], {stdin => $init_file});
342        }
343        else {
344            $self->write_config();
345            $self->run_command([$self->start_command, '--initialize']);
346            $self->start;
347            $self->load_sql("", $init_file);
348        }
349    }
350    else {
351        # Bootstrap is much faster without InnoDB, we will turn InnoDB back on later, and things will use it.
352        $self->write_config(skip => qr/innodb/i, add => {'default-storage-engine' => 'MyISAM'});
353        $self->run_command([$self->start_command, '--bootstrap'], {stdin => $init_file});
354
355        # Turn InnoDB back on
356        $self->write_config();
357    }
358
359    return;
360}
361
362sub load_sql {
363    my $self = shift;
364    my ($db_name, $file) = @_;
365
366    my $cfg_file = $self->{+CFG_FILE};
367
368    $self->run_command(
369        [
370            $self->{+MYSQL},
371            "--defaults-file=$cfg_file",
372            '-u' => 'root',
373            $db_name,
374        ],
375        {stdin => $file},
376    );
377}
378
379sub shell_command {
380    my $self = shift;
381    my ($db_name) = @_;
382
383    my $cfg_file = $self->{+CFG_FILE};
384    return ($self->{+MYSQL}, "--defaults-file=$cfg_file", $db_name);
385}
386
387sub start_command {
388    my $self = shift;
389
390    my $cfg_file = $self->{+CFG_FILE};
391    return ($self->{+MYSQLD}, "--defaults-file=$cfg_file", '--skip-grant-tables');
392}
393
394sub connect_string {
395    my $self = shift;
396    my ($db_name) = @_;
397    $db_name = 'quickdb' unless defined $db_name;
398
399    my $socket = $self->{+SOCKET};
400
401    if ($self->{+DBD_DRIVER} eq 'DBD::MariaDB') {
402        return "dbi:MariaDB:dbname=$db_name;mariadb_socket=$socket";
403    }
404    else {
405        return "dbi:mysql:dbname=$db_name;mysql_socket=$socket";
406    }
407}
408
4091;
410
411__END__
412
413=pod
414
415=encoding UTF-8
416
417=head1 NAME
418
419DBIx::QuickDB::Driver::MySQL - MySQL driver for DBIx::QuickDB.
420
421=head1 DESCRIPTION
422
423MySQL driver for L<DBIx::QuickDB>.
424
425=head1 SYNOPSIS
426
427See L<DBIx::QuickDB>.
428
429=head1 MYSQL SPECIFIC OPTIONS
430
431=over 4
432
433=item dbd_driver => $DRIVER
434
435Should be either L<DBD::mysql> or L<DBD::MariaDB>. If not specified then
436DBD::MariaDB is preferred with a fallback to DBD::MySQL.
437
438=item mysqld_provider => $PROVIDER
439
440Should be either 'mariadb' or 'percona'. Will auto-detect when possible.
441
442=head1 SOURCE
443
444The source code repository for DBIx-QuickDB can be found at
445F<https://github.com/exodist/DBIx-QuickDB/>.
446
447=head1 MAINTAINERS
448
449=over 4
450
451=item Chad Granum E<lt>exodist@cpan.orgE<gt>
452
453=back
454
455=head1 AUTHORS
456
457=over 4
458
459=item Chad Granum E<lt>exodist@cpan.orgE<gt>
460
461=back
462
463=head1 COPYRIGHT
464
465Copyright 2020 Chad Granum E<lt>exodist7@gmail.comE<gt>.
466
467This program is free software; you can redistribute it and/or
468modify it under the same terms as Perl itself.
469
470See F<http://dev.perl.org/licenses/>
471
472=cut
473