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